Browse Source

Misc bugfixes, advancements in post moderation.

Ralfp 12 years ago
parent
commit
13dfb2c490

+ 3 - 3
misago/forums/models.py

@@ -158,8 +158,8 @@ class Forum(MPTTModel):
         pass
     
     def sync(self):
-        self.threads = self.thread_set.filter(moderated=0).filter(deleted=0).count()
-        self.posts = self.post_set.filter(moderated=0).filter(deleted=0).count()
+        self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
+        self.posts = self.post_set.filter(moderated=False).filter(deleted=False).count()
         self.last_poster = None
         self.last_poster_name = None
         self.last_poster_slug = None
@@ -169,7 +169,7 @@ class Forum(MPTTModel):
         self.last_thread_name = None
         self.last_thread_slug = None
         try:
-            last_thread = self.thread_set.filter(moderated=0).filter(deleted=0).order_by('-last').all()[1:][0]
+            last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[0:][0]
             self.last_poster_name = last_thread.last_poster_name
             self.last_poster_slug = last_thread.last_poster_slug
             self.last_poster_style = last_thread.last_poster_style

+ 98 - 1
misago/threads/acl.py

@@ -154,6 +154,8 @@ class ThreadsACL(BaseACL):
                 raise ACLError404()
             if thread.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == thread.start_poster)):
                 raise ACLError404()
+            if thread.deleted and not forum_role['can_delete_threads']:
+                raise ACLError404()
         except KeyError:
             raise ACLError403(_("You don't have permission to read threads in this forum."))
     
@@ -181,6 +183,8 @@ class ThreadsACL(BaseACL):
                     queryset = queryset.filter(moderated=0)
             if forum_role['can_read_threads'] == 1:
                 queryset = queryset.filter(Q(weight=2) | Q(start_poster_id=request.user.id))
+            if not forum_role['can_delete_threads']:
+                queryset = queryset.filter(deleted=False)
         except KeyError:
             return False
         return queryset
@@ -234,6 +238,7 @@ class ThreadsACL(BaseACL):
     def allow_thread_edit(self, user, forum, thread, post):
         try:
             forum_role = self.acl[thread.forum_id]
+            self.allow_deleted_post_view(forum)
             if not forum_role['can_close_threads']:
                 if forum.closed:
                     raise ACLError403(_("You can't edit threads in closed forums."))
@@ -289,6 +294,7 @@ class ThreadsACL(BaseACL):
     def allow_reply_edit(self, user, forum, thread, post):
         try:
             forum_role = self.acl[thread.forum_id]
+            self.allow_deleted_post_view(forum)
             if not forum_role['can_close_threads']:
                 if forum.closed:
                     raise ACLError403(_("You can't edit replies in closed forums."))
@@ -314,6 +320,7 @@ class ThreadsACL(BaseACL):
     def allow_changelog_view(self, user, forum, post):
         try:
             forum_role = self.acl[forum.pk]
+            self.allow_deleted_post_view(forum)
             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:
@@ -376,11 +383,101 @@ class ThreadsACL(BaseACL):
         
     def can_protect(self, forum):
         try:
-            forum_role = self.acl[thread.forum.pk]
+            forum_role = self.acl[forum.pk]
             return forum_role['can_protect_posts']
         except KeyError:
             return False
 
+    def can_delete_thread(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
+                return False
+            if post.protected and not forum_role['can_protect_posts']:
+                return False
+            if forum_role['can_delete_threads']:
+                return forum_role['can_delete_threads']
+            if thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads']:
+                return 1
+            return False
+        except KeyError:
+            return False
+        
+    def allow_delete_thread(self, user, forum, thread, post, delete=False):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You don't have permission to delete threads in closed forum."))
+                if thread.closed:
+                    raise ACLError403(_("This thread is closed, you cannot delete it."))
+            if post.protected and not forum_role['can_protect_posts']:
+                raise ACLError403(_("This post is protected, you cannot delete it."))
+            if delete and forum_role['can_delete_threads'] < 2:
+                raise ACLError403(_("You cannot hard delete this thread."))
+            if not (forum_role['can_delete_threads'] or (thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads'])):
+                raise ACLError403(_("You don't have permission to delete this thread."))
+            if thread.deleted and not delete:
+                raise ACLError403(_("This thread is already deleted."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to delete this thread."))
+
+    def can_delete_post(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
+                return False
+            if post.protected and not forum_role['can_protect_posts']:
+                return False
+            if forum_role['can_delete_posts']:
+                return forum_role['can_delete_posts']
+            if post.user_id == user.pk and not post.protected and forum_role['can_soft_delete_own_posts']:
+                return 1
+            return False
+        except KeyError:
+            return False
+
+    def allow_delete_post(self, user, forum, thread, post, delete=False):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You don't have permission to delete posts in closed forum."))
+                if thread.closed:
+                    raise ACLError403(_("This thread is closed, you cannot delete its posts."))
+            if post.protected and not forum_role['can_protect_posts']:
+                raise ACLError403(_("This post is protected, you cannot delete it."))
+            if delete and forum_role['can_delete_posts'] < 2:
+                raise ACLError403(_("You cannot hard delete this post."))
+            if not (forum_role['can_delete_posts'] or (post.user_id == user.pk and forum_role['can_soft_delete_own_posts'])):
+                raise ACLError403(_("You don't have permission to delete this post."))
+            if post.deleted and not delete:
+                raise ACLError403(_("This post is already deleted."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to delete this post."))
+    
+    def can_see_deleted_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_delete_threads']
+        except KeyError:
+            raise false
+        
+    def can_see_deleted_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_delete_posts']
+        except KeyError:
+            raise false
+        
+    def allow_deleted_post_view(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_posts']:
+                raise ACLError404()
+        except KeyError:
+            raise ACLError404()
+
 
 def build_forums(acl, perms, forums, forum_roles):
     acl.threads = ThreadsACL()

+ 4 - 0
misago/threads/forms.py

@@ -71,6 +71,10 @@ class PostForm(Form, ThreadNameMixin):
         
         
 
+class SplitThreadForm(Form, ThreadNameMixin):
+    pass
+
+
 class QuickReplyForm(Form):
     post = forms.CharField(widget=forms.Textarea)
 

+ 11 - 6
misago/threads/models.py

@@ -46,6 +46,13 @@ class Thread(models.Model):
         return self.start
     
     def sync(self):
+        # Counters
+        self.replies = self.post_set.filter(moderated=False).filter(deleted=False).count() - 1
+        if self.replies < 0:
+            self.replies = 0
+        self.replies_reported = self.post_set.filter(reported=True).count()
+        self.replies_moderated = self.post_set.filter(moderated=True).count()
+        self.replies_deleted = self.post_set.filter(deleted=True).count()
         # First post
         start_post = self.post_set.order_by('merge', 'id')[0:][0]
         self.start = start_post.date
@@ -57,7 +64,10 @@ class Thread(models.Model):
         self.upvotes = start_post.upvotes
         self.downvotes = start_post.downvotes
         # Last post
-        last_post = self.post_set.order_by('-merge', '-id').filter(moderated=False).filter(deleted=False)[0:][0]
+        if self.replies > 0:
+            last_post = self.post_set.order_by('-merge', '-id').filter(moderated=False).filter(deleted=False)[0:][0]
+        else:
+            last_post = start_post
         self.last = last_post.date
         self.last_post = last_post
         self.last_poster = last_post.user
@@ -68,11 +78,6 @@ class Thread(models.Model):
         self.moderated = start_post.moderated
         self.deleted = start_post.deleted
         self.merges = last_post.merge
-        # Counters
-        self.replies = self.post_set.filter(moderated=False).filter(deleted=False).count() - 1
-        self.replies_reported = self.post_set.filter(reported=True).count()
-        self.replies_moderated = self.post_set.filter(moderated=True).count()
-        self.replies_deleted = self.post_set.filter(deleted=True).count()
     
 
 class PostManager(models.Manager):

+ 4 - 0
misago/threads/urls.py

@@ -15,6 +15,10 @@ urlpatterns = patterns('misago.threads.views',
     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+)/delete/$', 'DeleteView', name="thread_delete", kwargs={'mode': 'delete_thread'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'DeleteView', name="thread_hide", kwargs={'mode': 'hide_thread'}),
+    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+)/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"),

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

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

+ 11 - 1
misago/threads/views/base.py

@@ -1,4 +1,14 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from misago.utils import make_pagination
+
 class BaseView(object):
     def __new__(cls, request, **kwargs):
         obj = super(BaseView, cls).__new__(cls)
-        return obj(request, **kwargs)
+        return obj(request, **kwargs)
+    
+    def redirect_to_post(self, post):
+        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' % post.pk))
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))

+ 105 - 0
misago/threads/views/delete.py

@@ -0,0 +1,105 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago.acl.utils import ACLError403, ACLError404
+from misago.forums.models import Forum
+from misago.messages import Message
+from misago.threads.models import Thread, Post
+from misago.threads.views.base import BaseView
+from misago.views import error403, error404
+from misago.utils import make_pagination
+
+class DeleteView(BaseView):
+    def fetch_thread(self, kwargs):
+        self.thread = Thread.objects.get(pk=kwargs['thread'])
+        self.forum = self.thread.forum
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+        if self.mode in ['tread_delete', 'hide_thread']:
+            self.request.acl.threads.allow_delete_thread(
+                                                         self.request.user,
+                                                         self.forum,
+                                                         self.thread,
+                                                         self.thread.start_post,
+                                                         self.mode == 'delete_thread')
+            # Assert we are not user trying to delete thread with replies
+            acl = self.request.acl.threads.get_role(self.thread.forum_id)
+            if not acl['can_delete_threads']:
+                if self.thread.post_set.exclude(user_id=self.request.user.id).count() > 0:
+                    raise ACLError403(_("Somebody has already replied to this thread. You cannot delete it."))
+            
+    def fetch_post(self, kwargs):
+        self.post = self.thread.post_set.get(pk=kwargs['post'])
+        if self.post.pk == self.thread.start_post_id:
+            raise Post.DoesNotExist()
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        self.request.acl.threads.allow_delete_post(
+                                                   self.request.user,
+                                                   self.forum,
+                                                   self.thread,
+                                                   self.post,
+                                                   self.mode == 'delete_post')
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_posts'] and self.thread.post_set.filter(id__gt=self.post.pk).count() > 0:
+            raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
+        
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.mode = kwargs['mode']
+        try:
+            if not request.user.is_authenticated():
+                raise ACLError403(_("Guest, you have to sign-in in order to be able to delete replies."))
+            self.fetch_thread(kwargs)
+            if self.mode in ['hide_post', 'delete_post']:
+                self.fetch_post(kwargs)
+        except (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)
+        
+        if self.mode == 'delete_thread':
+            self.thread.delete()
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+            return redirect(reverse('forum', kwargs={'forum': self.thread.forum.pk, 'slug': self.thread.forum.slug}))
+        
+        if self.mode == 'hide_thread':
+            self.thread.start_post.deleted = True
+            self.thread.start_post.save(force_update=True)
+            self.thread.last_post.set_checkpoint(request, 'deleted')
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+            if request.acl.threads.can_see_deleted_threads(self.thread.forum):
+                return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+            return redirect(reverse('forum', kwargs={'forum': self.thread.forum.pk, 'slug': self.thread.forum.slug}))
+        
+        if self.mode == 'delete_post':
+            self.post.delete()
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            request.messages.set_flash(Message(_("Selected Reply has been deleted.")), 'success', 'threads')
+            return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+            
+        if self.mode == 'hide_post':
+            self.post.deleted = True
+            self.post.edit_date = timezone.now()
+            self.post.edit_user = request.user
+            self.post.edit_user_name = request.user.username
+            self.post.edit_user_slug = request.user.username_slug
+            self.post.save(force_update=True)
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            request.messages.set_flash(Message(_("Selected Reply has been deleted.")), 'success', 'threads_%s' % self.post.pk)
+            return self.redirect_to_post(self.post)

+ 3 - 3
misago/threads/views/posting.py

@@ -51,7 +51,7 @@ class PostingView(BaseView):
         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)     
+            self.request.acl.threads.allow_reply_edit(self.request.user, self.proxy, self.thread, self.post)     
         
     def get_form(self, bound=False):
         initial = {}
@@ -104,7 +104,7 @@ class PostingView(BaseView):
                     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_name = (old_name != form.cleaned_data['thread_name']) if self.mode == 'edit_thread' else False
                     changed_post = old_post != form.cleaned_data['post']
                     changed_anything = changed_name or changed_post
                 
@@ -191,7 +191,7 @@ class PostingView(BaseView):
                                                 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_old=old_name if self.mode == 'edit_thread' and 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,
                                                 )

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

@@ -7,9 +7,10 @@ from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.forms import Form, FormLayout, FormFields
 from misago.forums.models import Forum
+from misago.markdown import post_markdown
 from misago.messages import Message
 from misago.readstracker.trackers import ThreadsTracker
-from misago.threads.forms import MoveThreadsForm, QuickReplyForm
+from misago.threads.forms import MoveThreadsForm, SplitThreadForm, QuickReplyForm
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
@@ -111,6 +112,81 @@ class ThreadView(BaseView):
         else:
             self.posts_form = self.posts_form(request=self.request)
             
+    def post_action_accept(self, ids):
+        accepted = 0
+        for post in self.posts:
+            if post.pk in ids and post.moderated:
+                accepted += 1
+        if accepted:
+            self.thread.post_set.filter(id__in=ids).update(moderated=False)
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been accepted and made visible to other members.')), 'success', 'threads')
+            
+    def post_action_protect(self, ids):
+        protected = 0
+        for post in self.posts:
+            if post.pk in ids and not post.protected:
+                protected += 1
+        if protected:
+            self.thread.post_set.filter(id__in=ids).update(protected=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been protected from edition.')), 'success', 'threads')            
+            
+    def post_action_merge(self, ids):
+        users = []
+        posts = []
+        for post in self.posts:
+            if post.pk in ids:
+                posts.append(post)
+                if not post.user_id in users:
+                    users.append(post.user_id)
+                if len(users) > 1:
+                    raise forms.ValidationError(_("You cannot merge replies made by different members!"))
+        if len(posts) < 2:
+            raise forms.ValidationError(_("You have to select two or more posts you want to merge."))
+        new_post = posts[0]
+        for post in posts[1:]:
+            new_post.post = '%s\n- - -\n%s' % (new_post.post, post.post)
+            post.change_set.update(post=new_post)
+            post.checkpoint_set.update(post=new_post)
+            post.delete()
+        new_post.post_preparsed = post_markdown(self.request, new_post.post)
+        new_post.save(force_update=True)
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        self.request.messages.set_flash(Message(_('Selected posts have been merged into one message.')), 'success', 'threads')
+                    
+    def post_action_split(self, ids):
+        message = None
+        if self.request.POST.get('do') == 'split':
+            form = SplitThreadForm(self.request.POST,request=self.request)
+            if form.is_valid():
+                return None
+            message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = SplitThreadForm(request=self.request)
+        return self.request.theme.render_to_response('threads/split.html',
+                                                     {
+                                                      'message': message,
+                                                      'forum': self.forum,
+                                                      'parents': self.parents,
+                                                      'thread': self.thread,
+                                                      'posts': ids,
+                                                      'form': FormLayout(form),
+                                                      },
+                                                     context_instance=RequestContext(self.request));
+        
+    def post_action_unprotect(self, ids):
+        unprotected = 0
+        for post in self.posts:
+            if post.pk in ids and post.protected:
+                unprotected += 1
+        if unprotected:
+            self.thread.post_set.filter(id__in=ids).update(protected=False)
+            self.request.messages.set_flash(Message(_('Protection from editions has been removed from selected posts.')), 'success', 'threads')
+                
     def get_thread_actions(self):
         acl = self.request.acl.threads.get_role(self.thread.forum_id)
         actions = []
@@ -306,6 +382,7 @@ class ThreadView(BaseView):
         try:
             self.fetch_thread(thread)
             self.fetch_posts(page)
+            self.message = request.messages.get_message('threads')
             self.make_thread_form()
             if self.thread_form:
                 response = self.handle_thread_form()
@@ -326,7 +403,7 @@ class ThreadView(BaseView):
         self.forum.closed = self.proxy.closed
         return request.theme.render_to_response('threads/thread.html',
                                                 {
-                                                 'message': request.messages.get_message('threads'),
+                                                 'message': self.message,
                                                  'forum': self.forum,
                                                  'parents': self.parents,
                                                  'thread': self.thread,

+ 3 - 0
static/sora/css/sora.css

@@ -1009,6 +1009,9 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .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 hr{background-color:#999999;background-image:-webkit-gradient(linear, 0 0, 100% 100%, color-stop(0.25, rgba(255, 255, 255, 0.2)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.2)), color-stop(0.75, rgba(255, 255, 255, 0.2)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);-webkit-background-size:10px 10px;-moz-background-size:10px 10px;background-size:10px 10px;border:none;margin:0px;margin-top:8px;height:4px;}
+.posts-list .well-post-deleted{opacity:0.5;filter:alpha(opacity=50);padding:6px 0px;}.posts-list .well-post-deleted .post-author{bottom:2px;}.posts-list .well-post-deleted .post-author .post-bit .lead{padding:0px;}
+.posts-list .well-post-deleted .post-content{position:relative;top:8px;}.posts-list .well-post-deleted .post-content a{font-weight:bold;}
+.posts-list .well-post-deleted .post-extra{position:relative;top:7px;}
 .mod-actions{overflow:auto;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin-top:0px;}
 .quick-reply{margin-top:12px;overflow:auto;}.quick-reply .avatar-big,.quick-reply .arrow{float:left;}
 .quick-reply .arrow{width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-right:12px solid #e3e3e3;position:relative;top:12px;left:5px;}

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

@@ -337,6 +337,35 @@
       height: 4px;
     }
   }
+  
+  .well-post-deleted {
+    .opacity(50);
+    padding: 6px 0px;
+    
+    .post-author {
+      bottom: 2px;
+      
+      .post-bit {        
+        .lead {
+          padding: 0px;
+        }
+      }
+    }
+    
+    .post-content {
+      position: relative;
+      top: 8px;
+      
+      a {
+        font-weight: bold;
+      }
+    }
+    
+    .post-extra {
+      position: relative;
+      top: 7px;      
+    }
+  }
 }
 
 // Thread mod actions

+ 1 - 0
templates/sora/threads/split.html

@@ -0,0 +1 @@
+SPLIT THREAD!

+ 47 - 3
templates/sora/threads/thread.html

@@ -45,6 +45,29 @@
 <div class="posts-list">
   {% for post in posts %}
   {% if post.message %}{{ macros.draw_message(post.message) }}{% endif %}
+  {% 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 class="post-author">
+      <div class="post-bit">
+        {% if post.user_id %}
+        <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="lead">{{ post.user.username }}</a>
+        {% else %}
+        <span class="lead">{{ post.user_name }}</span>
+        {% endif %}
+      </div>
+    </div>
+    <div class="post-extra">
+      <a href="{% if pagination['page'] > 1 -%}
+      {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
+      {%- else -%}
+      {% url 'thread' thread=thread.pk, slug=thread.slug %}
+      {%- endif %}#post-{{ post.pk }}" class="post-perma pull-right tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+    </div>
+    <div class="post-content">
+      {% trans user=edit_user(post), date=post.edit_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+    </div>
+  </div>
+  {% else %}
   <div id="post-{{ post.pk }}" class="well well-post{% if post.user and post.user.rank and post.user.rank.style %} {{ post.user.rank.style }}{% endif %}">
     <div class="post-author">
       <img src="{% if post.user_id %}{{ post.user.get_avatar(80) }}{% else %}{{ macros.avatar_guest(80) }}{% endif %}" alt="" class="avatar-normal">
@@ -121,11 +144,19 @@
       {% endif %}
       {% if user.is_authenticated() %}
       <ul class="nav nav-pills pull-right">
-        <li class="tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}"><button class="btn danger"><i class="icon-remove"></i></button></li>
-        <li><button class="btn danger"><i class="icon-trash"></i> {% trans %}Delete{% endtrans %}</button></li>
         {% 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>
+        {% endif %}
+        {% if post.pk == thread.start_post_id %}
+        {% 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 %}" 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 %}
+        {% if not post.deleted and acl.threads.can_delete_thread(user, forum, thread, post) -%}
+            <li><form action="{% url 'thread_hide' thread=thread.pk, slug=thread.slug %}" method="post"><button type="submit" class="btn danger"><i class="icon-trash"></i> {% trans %}Delete{% endtrans %}</button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}
+        {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
+        {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
+            <li class="tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}"><form action="{% url 'post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" 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 %}
+        {% if not post.deleted and acl.threads.can_delete_post(user, forum, thread, post) -%}
+            <li><form action="{% url 'post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" method="post"><button type="submit" class="btn danger"><i class="icon-trash"></i> {% trans %}Delete{% endtrans %}</button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}
         {% 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>
@@ -137,6 +168,7 @@
       {% endif %}
     </div>
   </div>
+  {% endif %}
   {% if post.checkpoint_set.all() %}
   <div class="post-checkpoints">
     {% for checkpoint in post.checkpoint_set.all() %}
@@ -227,7 +259,19 @@
 {% endmacro %}
 
 {% macro checkpoint_user(checkpoint) -%}
+{%- if checkpoint.user_id -%}
 {{ ('<a href="' ~ 'user'|url(user=checkpoint.user_id, username=checkpoint.user_slug) ~ '">')|safe ~ (checkpoint.user_name) ~ ("</a>")|safe }}
+{%- else -%}
+<strong>{{ checkpoint.user_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+{% macro edit_user(post) -%}
+{%- if post.edit_user_id -%}
+{{ ('<a href="' ~ 'user'|url(user=post.edit_user_id, username=post.edit_user_slug) ~ '">')|safe ~ (post.edit_user_name) ~ ("</a>")|safe }}
+{%- else -%}
+<strong>{{ post.edit_user_name }}</strong>
+{%- endif -%}
 {%- endmacro %}
 
 {% block javascripts -%}