Browse Source

- Thread Moderation from ThreadView level

Ralfp 12 years ago
parent
commit
06ba8bb35d

+ 4 - 1
misago/threads/acl.py

@@ -138,7 +138,10 @@ def make_forum_form(request, role, form):
 class ThreadsACL(BaseACL):
     def get_role(self, forum):
         try:
-            return self.acl[forum.pk]
+            try:
+                return self.acl[forum.pk]
+            except AttributeError:
+                return self.acl[forum]
         except KeyError:
             return {}
     

+ 89 - 45
misago/threads/views/list.py

@@ -1,23 +1,23 @@
 from django.core.urlresolvers import reverse
 from django.db.models import Q, F
+from django import forms
 from django.forms import ValidationError
 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.forms import FormLayout, FormFields
+from misago.forms import Form, FormLayout, FormFields
 from misago.forums.models import Forum
 from misago.messages import Message
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
 from misago.threads.forms import MoveThreadsForm, MergeThreadsForm
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
-from misago.threads.views.mixins import ThreadsFormMixin
 from misago.views import error403, error404
 from misago.utils import make_pagination, slugify
 
-class ThreadsView(BaseView, ThreadsFormMixin):
+class ThreadsView(BaseView):
     def fetch_forum(self, forum):
         self.forum = Forum.objects.get(pk=forum, type='forum')
         self.proxy = Forum.objects.parents_aware_forum(self.forum)
@@ -50,32 +50,89 @@ class ThreadsView(BaseView, ThreadsFormMixin):
         actions = []
         try:
             if acl['can_approve']:
-               actions.append(('accept', _('Accept threads')))
+                actions.append(('accept', _('Accept threads')))
             if acl['can_pin_threads'] == 2:
-               actions.append(('annouce', _('Change to annoucements')))
+                actions.append(('annouce', _('Change to annoucements')))
             if acl['can_pin_threads'] > 0:
-               actions.append(('sticky', _('Change to sticky threads')))
+                actions.append(('sticky', _('Change to sticky threads')))
             if acl['can_pin_threads'] > 0:
-               actions.append(('normal', _('Change to standard thread')))
+                actions.append(('normal', _('Change to standard thread')))
             if acl['can_move_threads_posts']:
-               actions.append(('move', _('Move threads')))
-               actions.append(('merge', _('Merge threads')))
+                actions.append(('move', _('Move threads')))
+                actions.append(('merge', _('Merge threads')))
             if acl['can_close_threads']:
-               actions.append(('open', _('Open threads')))
-               actions.append(('close', _('Close threads')))
+                actions.append(('open', _('Open threads')))
+                actions.append(('close', _('Close threads')))
             if acl['can_delete_threads']:
-               actions.append(('undelete', _('Undelete threads')))
+                actions.append(('undelete', _('Undelete threads')))
             if acl['can_delete_threads']:
-               actions.append(('soft', _('Soft delete threads')))
+                actions.append(('soft', _('Soft delete threads')))
             if acl['can_delete_threads'] == 2:
-               actions.append(('hard', _('Hard delete threads')))
+                actions.append(('hard', _('Hard delete threads')))
         except KeyError:
             pass
         return actions
     
+    def make_form(self):
+        self.form = None
+        list_choices = self.get_thread_actions();
+        if (not self.request.user.is_authenticated()
+            or not list_choices):
+            return
+        
+        form_fields = {}
+        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
+        list_choices = []
+        for item in self.threads:
+            if item.forum_id == self.forum.pk:
+                list_choices.append((item.pk, None))
+        if not list_choices:
+            return
+        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices,widget=forms.CheckboxSelectMultiple)
+        self.form = type('ThreadsViewForm', (Form,), form_fields)
+    
+    def handle_form(self):
+        if self.request.method == 'POST':
+            self.form = self.form(self.request.POST, request=self.request)
+            if self.form.is_valid():
+                checked_items = []
+                posts = []
+                for thread in self.threads:
+                    if str(thread.pk) in self.form.cleaned_data['list_items'] and thread.forum_id == self.forum.pk:
+                        posts.append(thread.start_post_id)
+                        if thread.start_post_id != thread.last_post_id:
+                            posts.append(thread.last_post_id)
+                        checked_items.append(thread.pk)
+                if checked_items:
+                    if posts:
+                        for post in Post.objects.filter(id__in=posts).prefetch_related('user'):
+                            for thread in self.threads:
+                                if thread.start_post_id == post.pk:
+                                    thread.start_post = post
+                                if thread.last_post_id == post.pk:
+                                    thread.last_post = post
+                                if thread.start_post_id == post.pk or thread.last_post_id == post.pk:
+                                    break
+                    form_action = getattr(self, 'action_' + self.form.cleaned_data['list_action'])
+                    try:
+                        response = form_action(checked_items)
+                        if response:
+                            return response
+                        return redirect(self.request.path)
+                    except forms.ValidationError as e:
+                        self.message = Message(e.messages[0], 'error')
+                else:
+                    self.message = Message(_("You have to select at least one thread."), 'error')
+            else:
+                if 'list_action' in self.form.errors:
+                    self.message = Message(_("Action requested is incorrect."), 'error')
+                else:
+                    self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            self.form = self.form(request=self.request)
+            
     def action_accept(self, ids):
         accepted = 0
-        posts = 0
         users = []
         for thread in self.threads:
             if thread.pk in ids and thread.moderated:
@@ -86,30 +143,18 @@ class ThreadsView(BaseView, ThreadsFormMixin):
                 thread.save(force_update=True)
                 thread.start_post.moderated = False
                 thread.start_post.save(force_update=True)
+                thread.last_post.set_checkpoint(self.request, 'accepted')
                 # Sync user
                 if thread.last_post.user:
                     thread.start_post.user.threads += 1
                     thread.start_post.user.posts += 1
                     users.append(thread.start_post.user)
-                    thread.last_post.set_checkpoint(self.request, 'accepted')
-                # Sync forum
-                self.forum.threads += 1
-                self.forum.threads_delta += 1
-                self.forum.posts += thread.replies + 1
-                posts += thread.replies + 1
-                self.forum.posts_delta += thread.replies + 1
-                if not self.forum.last_thread_date or self.forum.last_thread_date < thread.last:
-                    self.forum.last_thread = thread
-                    self.forum.last_thread_name = thread.name
-                    self.forum.last_thread_slug = thread.slug
-                    self.forum.last_thread_date = thread.last
-                    self.forum.last_poster = thread.last_poster
-                    self.forum.last_poster_name = thread.last_poster_name
-                    self.forum.last_poster_slug = thread.last_poster_slug
-                    self.forum.last_poster_style = thread.last_poster_style
         if accepted:
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
-            self.request.monitor['posts'] = posts
+            self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
+            self.forum.threads_delta += 1
+            self.forum.posts_delta += self.thread.replies + 1
+            self.forum.sync()
             self.forum.save(force_update=True)
             for user in users:
                 user.save(force_update=True)
@@ -142,16 +187,6 @@ class ThreadsView(BaseView, ThreadsFormMixin):
         if normalised:
             Thread.objects.filter(id__in=normalised).update(weight=0)
             self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
-        
-    def action_open(self, ids):
-        opened = []
-        for thread in self.threads:
-            if thread.pk in ids and thread.closed:
-                opened.append(thread.pk)
-                thread.last_post.set_checkpoint(self.request, 'opened')
-        if opened:
-            Thread.objects.filter(id__in=opened).update(closed=False)
-            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
     
     def action_move(self, ids):
         threads = []
@@ -234,7 +269,17 @@ class ThreadsView(BaseView, ThreadsFormMixin):
                                                       'threads': threads,
                                                       'form': FormLayout(form),
                                                       },
-                                                     context_instance=RequestContext(self.request));      
+                                                     context_instance=RequestContext(self.request)); 
+    
+    def action_open(self, ids):
+        opened = []
+        for thread in self.threads:
+            if thread.pk in ids and thread.closed:
+                opened.append(thread.pk)
+                thread.last_post.set_checkpoint(self.request, 'opened')
+        if opened:
+            Thread.objects.filter(id__in=opened).update(closed=False)
+            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')     
         
     def action_close(self, ids):
         closed = []
@@ -259,8 +304,7 @@ class ThreadsView(BaseView, ThreadsFormMixin):
         if undeleted:
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + len(undeleted)
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + posts
-            self.forum.threads += len(undeleted)
-            self.forum.posts += posts
+            self.forum.sync()
             self.forum.save(force_update=True)
             Thread.objects.filter(id__in=undeleted).update(deleted=False)
             self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')

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

@@ -1,65 +0,0 @@
-from django import forms
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.forms import Form
-from misago.messages import Message
-from misago.threads.models import Post
-
-class ThreadsFormMixin(object):
-    def make_form(self):
-        self.form = None
-        list_choices = self.get_thread_actions();
-        if (not self.request.user.is_authenticated()
-            or not list_choices):
-            return
-        
-        form_fields = {}
-        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
-        list_choices = []
-        for item in self.threads:
-            if item.forum_id == self.forum.pk:
-                list_choices.append((item.pk, None))
-        if not list_choices:
-            return
-        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices,widget=forms.CheckboxSelectMultiple)
-        self.form = type('ThreadsViewForm', (Form,), form_fields)
-    
-    def handle_form(self):
-        if self.request.method == 'POST':
-            self.form = self.form(self.request.POST, request=self.request)
-            if self.form.is_valid():
-                checked_items = []
-                posts = []
-                for thread in self.threads:
-                    if str(thread.pk) in self.form.cleaned_data['list_items'] and thread.forum_id == self.forum.pk:
-                        posts.append(thread.start_post_id)
-                        if thread.start_post_id != thread.last_post_id:
-                            posts.append(thread.last_post_id)
-                        checked_items.append(thread.pk)
-                if checked_items:
-                    if posts:
-                        for post in Post.objects.filter(id__in=posts).prefetch_related('user'):
-                            for thread in self.threads:
-                                if thread.start_post_id == post.pk:
-                                    thread.start_post = post
-                                if thread.last_post_id == post.pk:
-                                    thread.last_post = post
-                                if thread.start_post_id == post.pk or thread.last_post_id == post.pk:
-                                    break
-                    form_action = getattr(self, 'action_' + self.form.cleaned_data['list_action'])
-                    try:
-                        response = form_action(checked_items)
-                        if response:
-                            return response
-                        return redirect(self.request.path)
-                    except forms.ValidationError as e:
-                        self.message = Message(e.messages[0], 'error')
-                else:
-                    self.message = Message(_("You have to select at least one thread."), 'error')
-            else:
-                if 'list_action' in self.form.errors:
-                    self.message = Message(_("Action requested is incorrect."), 'error')
-                else:
-                    self.message = Message(form.non_field_errors()[0], 'error')
-        else:
-            self.form = self.form(request=self.request)

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

@@ -213,7 +213,7 @@ class PostingView(BaseView):
                             post.set_checkpoint(self.request, 'limit')
                 
                 # Update last poster data
-                if not moderation:
+                if not moderation and self.mode not in ['edit_thread', 'edit_post']:
                     thread.last = now
                     thread.last_post = post
                     thread.last_poster = request.user

+ 201 - 4
misago/threads/views/thread.py

@@ -1,12 +1,15 @@
 from django.core.urlresolvers import reverse
+from django import forms
+from django.forms import ValidationError
 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.forms import FormFields
+from misago.forms import Form, FormLayout, FormFields
 from misago.forums.models import Forum
+from misago.messages import Message
 from misago.readstracker.trackers import ThreadsTracker
-from misago.threads.forms import QuickReplyForm
+from misago.threads.forms import MoveThreadsForm, QuickReplyForm
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
@@ -40,7 +43,195 @@ class ThreadView(BaseView):
         if not self.tracker.is_read(self.thread):
             self.tracker.set_read(self.thread, last_post)
             self.tracker.sync()
-     
+    
+    def get_thread_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if acl['can_approve'] and self.thread.moderated:
+                actions.append(('accept', _('Accept this thread')))
+            if acl['can_pin_threads'] == 2 and self.thread.weight < 2:
+                actions.append(('annouce', _('Change this thread to annoucement')))
+            if acl['can_pin_threads'] > 0 and self.thread.weight != 1:
+                actions.append(('sticky', _('Change this thread to sticky')))
+            if acl['can_pin_threads'] > 0:
+                if self.thread.weight == 2:
+                    actions.append(('normal', _('Change this thread to normal')))
+                if self.thread.weight == 1:
+                    actions.append(('normal', _('Unpin this thread')))
+            if acl['can_move_threads_posts']:
+                actions.append(('move', _('Move this thread')))
+            if acl['can_close_threads']:
+                if self.thread.closed:
+                    actions.append(('open', _('Open this thread')))
+                else:
+                    actions.append(('close', _('Close this thread')))
+            if acl['can_delete_threads']:
+                if self.thread.deleted:
+                    actions.append(('undelete', _('Undelete this thread')))
+                else:
+                    actions.append(('soft', _('Soft delete this thread')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Hard delete this thread')))
+        except KeyError:
+            pass
+        return actions
+    
+    def make_thread_form(self):
+        self.thread_form = None
+        list_choices = self.get_thread_actions();
+        if (not self.request.user.is_authenticated()
+            or not list_choices):
+            return      
+        form_fields = {'thread_action': forms.ChoiceField(choices=list_choices)}
+        self.thread_form = type('ThreadViewForm', (Form,), form_fields)
+    
+    def handle_thread_form(self):
+        if self.request.method == 'POST' and self.request.POST.get('origin') == 'thread_form':
+            self.thread_form = self.thread_form(self.request.POST, request=self.request)
+            if self.thread_form.is_valid():
+                form_action = getattr(self, 'thread_action_' + self.thread_form.cleaned_data['thread_action'])
+                try:
+                    response = form_action()
+                    if response:
+                        return response
+                    return redirect(self.request.path)
+                except forms.ValidationError as e:
+                    self.message = Message(e.messages[0], 'error')
+            else:
+                if 'thread_action' in self.thread_form.errors:
+                    self.message = Message(_("Action requested is incorrect."), 'error')
+                else:
+                    self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            self.thread_form = self.thread_form(request=self.request)
+
+    def thread_action_accept(self):
+        # Sync thread and post
+        self.thread.moderated = False
+        self.thread.replies_moderated -= 1
+        self.thread.save(force_update=True)
+        self.thread.start_post.moderated = False
+        self.thread.start_post.save(force_update=True)
+        self.thread.last_post.set_checkpoint(self.request, 'accepted')
+        # Sync user
+        if self.thread.last_post.user:
+            self.thread.start_post.user.threads += 1
+            self.thread.start_post.user.posts += 1
+            self.thread.start_post.user.save(force_update=True)            
+        # Sync forum
+        self.forum.threads_delta += 1
+        self.forum.posts_delta += self.thread.replies + 1
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
+        self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
+        self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
+    
+    def thread_action_annouce(self):
+        self.thread.weight = 2
+        self.thread.save(force_update=True)
+        self.request.messages.set_flash(Message(_('Thread has been turned into annoucement.')), 'success', 'threads')
+    
+    def thread_action_sticky(self):
+        self.thread.weight = 1
+        self.thread.save(force_update=True)
+        self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
+    
+    def thread_action_normal(self):
+        self.thread.weight = 0
+        self.thread.save(force_update=True)
+        self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
+    
+    def thread_action_move(self):
+        message = None
+        if self.request.POST.get('do') == 'move':
+            form = MoveThreadsForm(self.request.POST,request=self.request,forum=self.forum)
+            if form.is_valid():
+                new_forum = form.cleaned_data['new_forum']
+                self.thread.forum = new_forum
+                self.thread.post_set.update(forum=new_forum)
+                self.thread.change_set.update(forum=new_forum)
+                self.thread.checkpoint_set.update(forum=new_forum)
+                self.thread.save(force_update=True)
+                self.forum.sync()
+                self.forum.save(force_update=True)
+                self.request.messages.set_flash(Message(_('Thread has been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
+                return None
+            message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = MoveThreadsForm(request=self.request,forum=self.forum)
+        return self.request.theme.render_to_response('threads/move.html',
+                                                     {
+                                                      'message': message,
+                                                      'forum': self.forum,
+                                                      'parents': self.parents,
+                                                      'thread': self.thread,
+                                                      'form': FormLayout(form),
+                                                      },
+                                                     context_instance=RequestContext(self.request));
+        
+    def thread_action_open(self):
+        self.thread.closed = False
+        self.thread.save(force_update=True)
+        self.thread.last_post.set_checkpoint(self.request, 'opened')
+        self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
+        
+    def thread_action_close(self):
+        self.thread.closed = True
+        self.thread.save(force_update=True)
+        self.thread.last_post.set_checkpoint(self.request, 'closed')
+        self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
+    
+    def thread_action_undelete(self):
+        # Update thread
+        self.thread.deleted = False
+        self.thread.replies_deleted -= 1
+        self.thread.save(force_update=True)
+        # Update first post in thread
+        self.thread.start_post.deleted = False
+        self.thread.start_post.save(force_update=True)
+        # Set checkpoint
+        self.thread.last_post.set_checkpoint(self.request, 'undeleted')
+        # Update forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
+        self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
+        self.request.messages.set_flash(Message(_('Thread has been undeleted.')), 'success', 'threads')
+    
+    def thread_action_soft(self):
+        # Update thread
+        self.thread.deleted = True
+        self.thread.replies_deleted += 1
+        self.thread.save(force_update=True)
+        # Update first post in thread
+        self.thread.start_post.deleted = True
+        self.thread.start_post.save(force_update=True)
+        # Set checkpoint
+        self.thread.last_post.set_checkpoint(self.request, 'deleted')
+        # Update forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
+        self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
+        self.request.messages.set_flash(Message(_('Thread has been deleted.')), 'success', 'threads')        
+    
+    def thread_action_hard(self):
+        # Delete thread
+        self.thread.delete()
+        # Update forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
+        self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
+        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+        return redirect(reverse('forum', kwargs={'forum': self.forum.pk, 'slug': self.forum.slug}))
+    
     def __call__(self, request, slug=None, thread=None, page=0):
         self.request = request
         self.pagination = None
@@ -48,6 +239,11 @@ class ThreadView(BaseView):
         try:
             self.fetch_thread(thread)
             self.fetch_posts(page)
+            self.make_thread_form()
+            if self.thread_form:
+                response = self.handle_thread_form()
+                if response:
+                    return response
         except Thread.DoesNotExist:
             return error404(self.request)
         except ACLError403 as e:
@@ -66,6 +262,7 @@ class ThreadView(BaseView):
                                                  'count': self.count,
                                                  'posts': self.posts,
                                                  'pagination': self.pagination,
-                                                 'quick_reply': FormFields(QuickReplyForm(request=request)).fields
+                                                 'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
+                                                 'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,
                                                  },
                                                 context_instance=RequestContext(request));

+ 5 - 4
static/sora/css/sora.css

@@ -992,11 +992,11 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .posts-list .well-post .post-author .post-bit span.lead{color:#cccccc;}
 .posts-list .well-post .post-author .post-bit .user-title{color:#999999;}
 .posts-list .well-post .post-author .post-bit .post-date{margin-top:4px;color:#999999;font-size:70%;font-weight:normal;}
-.posts-list .well-post .post-content{margin-left:286px;padding:0px 16px;}.posts-list .well-post .post-content .post-extra{overflow:auto;float:right;margin-bottom:12px;}.posts-list .well-post .post-content .post-extra .post-perma{margin-left:8px;color:#999999;}
-.posts-list .well-post .post-content .post-extra .label{margin-left:8px;padding:4px 5px;font-size:100%;}
-.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{margin-left:286px;margin-right:200px;padding:0px 16px;}.posts-list .well-post .post-content .post-foot{margin-top:20px;}.posts-list .well-post .post-content .post-foot .lead{margin:0px;color:#999999;font-size:100%;}.posts-list .well-post .post-content .post-foot .lead a{color:#999999;}
 .posts-list .well-post .post-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-extra{overflow:auto;float:right;width:200px;padding-right:16px;}.posts-list .well-post .post-extra .post-perma{margin-left:8px;color:#999999;}
+.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-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;}
@@ -1008,6 +1008,7 @@ 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;}
+.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;}
 .quick-reply .editor{margin-left:142px;}

+ 34 - 23
static/sora/css/sora/threads.less

@@ -160,30 +160,9 @@
 
     .post-content {
       margin-left: 286px;
+      margin-right: 200px;
       padding: 0px 16px;
-      
-      .post-extra {
-        overflow: auto;
-        float: right;
-        margin-bottom: 12px;
-        
-        .post-perma {
-          margin-left: 8px;
-          
-          color: @grayLight;
-        }
-        
-        .label {
-          margin-left: 8px;
-          padding: 4px 5px;
-          font-size: 100%;
-        }
-        
-        .label-purple {
-          background-color: @purple;
-        }
-      }
-      
+            
       .post-foot {
         margin-top: 20px;
         
@@ -209,6 +188,30 @@
       }
     }
     
+    .post-extra {
+      overflow: auto;
+      float: right;
+      width: 200px;
+      padding-right: 16px;
+      
+      .post-perma {
+        margin-left: 8px;
+        
+        color: @grayLight;
+      }
+      
+      .label {
+        margin-left: 8px;
+        margin-bottom: 8px;
+        padding: 4px 5px;
+        font-size: 100%;
+      }
+      
+      .label-purple {
+        background-color: @purple;
+      }
+    }
+    
     .post-nav {
       clear: both;
       margin-left: 286px;
@@ -331,6 +334,14 @@
   }
 }
 
+// Thread mod actions
+.mod-actions {
+  overflow: auto;
+  border: none;
+  .border-radius(3px);
+  margin-top: 0px;
+}
+
 // Quick reply box
 .quick-reply {
   margin-top: 12px;

+ 32 - 5
templates/sora/threads/move.html

@@ -4,14 +4,23 @@
 {% import "_forms.html" as form_theme with context %}
 {% import "sora/macros.html" as macros with context %}
 
-{% block title %}{{ macros.page_title(title=_("Move Threads"),parent=forum.name) }}{% endblock %}
+{% block title %}{% if thread is defined -%}
+{{ macros.page_title(title=_("Move Thread"),parent=thread.name) }}
+{%- else -%}
+{{ macros.page_title(title=_("Move Threads"),parent=forum.name) }}
+{%- endif %}{% endblock %}
 
 {% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
 {% for parent in parents %}
 <li class="first"><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
 {% endfor %}
-<li class="first"><a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}">{{ forum.name }}</a> <span class="divider">/</span></li>
+<li><a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}">{{ forum.name }}</a> <span class="divider">/</span></li>
+{% if thread is defined %}
+<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider">/</span></li>
+<li class="active">{% trans %}Move Thread{% endtrans %}
+{% else %}
 <li class="active">{% trans %}Move Threads{% endtrans %}
+{% endif %}
 {%- endblock %}
 
 {% block content %}
@@ -19,22 +28,40 @@
   <ul class="breadcrumb">
     {{ self.breadcrumb() }}</li>
   </ul>
+  {% if thread is defined %}
+  <h1>{% trans %}Move Thread{% endtrans %} <small>{{ thread.name }}</small></h1>
+  {% else %}
   <h1>{% trans %}Move Threads{% endtrans %} <small>{{ forum.name }}</small></h1>
+  {% endif %}
 </div>
 <div class="row">
   <div class="span8 offset2">
     {% if message %}{{ macros.draw_message(message) }}{% endif %}
-    <form action="{% url 'forum' forum=forum.pk, slug=forum.slug %}" method="post">
+    <form action="{% if thread is defined %}
+    {% url 'thread' thread=thread.pk, slug=thread.slug %}
+    {%- else -%}
+    {% url 'forum' forum=forum.pk, slug=forum.slug %}
+    {%- endif %}" method="post">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      {% if thread is defined %}
+      <input type="hidden" name="origin" value="thread_form">
+      <input type="hidden" name="thread_action" value="move">
+      <input type="hidden" name="do" value="move">
+      {% else %}
       <input type="hidden" name="origin" value="move_form">
       <input type="hidden" name="list_action" value="move">
       {% for thread in threads -%}
       <input type="hidden" name="list_items" value="{{ thread.pk }}">
       {% endfor %}
+      {% endif %}
       {{ form_theme.form_widget(form, width=8) }}
       <div class="form-actions">
-        <button name="save" type="submit" class="btn btn-primary">{% trans %}Move Threads{% endtrans %}</button>
-        <a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}" class="btn">{% trans %}Cancel{% endtrans %}</a>
+        <button name="save" type="submit" class="btn btn-primary">{% if thread is defined %}{% trans %}Move Thread{% endtrans %}{% else %}{% trans %}Move Threads{% endtrans %}{% endif %}</button>
+        <a href="{% if thread is defined -%}
+    {% url 'thread' thread=thread.pk, slug=thread.slug %}
+    {%- else -%}
+    {% url 'forum' forum=forum.pk, slug=forum.slug %}
+    {%- endif %}" class="btn">{% trans %}Cancel{% endtrans %}</a>
       </div>
     </form>
   </div>

+ 57 - 27
templates/sora/threads/thread.html

@@ -1,6 +1,7 @@
 {% extends "sora/layout.html" %}
 {% load i18n %}
 {% load url from future %}
+{% import "_forms.html" as form_theme with context %}
 {% import "sora/editor.html" as editor with context %}
 {% import "sora/macros.html" as macros with context %}
 
@@ -58,34 +59,34 @@
         <p class="post-date">{{ post.date|reltimesince|low }}</p>
       </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>
+      {% if not post.is_read %}
+      <span class="label label-warning pull-right">
+        {% trans %}New{% endtrans %}
+      </span>
+      {% endif %}
+      {% if post.moderated %}
+      <span class="label label-purple pull-right">
+        {% trans %}Unreviewed{% endtrans %}
+      </span>
+      {% endif %}
+      {% if post.reported %}
+      <span class="label label-important pull-right">
+        {% trans %}Reported{% endtrans %}
+      </span>
+      {% endif %}
+      {% if post.deleted %}
+      <span class="label label-inverse pull-right">
+        {% trans %}Deleted{% endtrans %}
+      </span>
+      {% endif %}
+    </div>
     <div class="post-content">
-      <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>
-        {% if not post.is_read %}
-        <span class="label label-warning pull-right">
-          {% trans %}New{% endtrans %}
-        </span>
-        {% endif %}
-        {% if post.moderated %}
-        <span class="label label-purple pull-right">
-          {% trans %}Unreviewed{% endtrans %}
-        </span>
-        {% endif %}
-        {% if post.reported %}
-        <span class="label label-important pull-right">
-          {% trans %}Reported{% endtrans %}
-        </span>
-        {% endif %}
-        {% if post.deleted %}
-        <span class="label label-inverse pull-right">
-          {% trans %}Deleted{% endtrans %}
-        </span>
-        {% endif %}
-      </div>
       <div class="markdown">
         {{ post.post_preparsed|safe }}
       </div>
@@ -112,6 +113,8 @@
       {% 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>
@@ -153,6 +156,17 @@
   {% endfor %}
 </div>
 
+{% if user.is_authenticated() and thread_form %}
+<div class="form-actions table-footer mod-actions">
+  <form id="thread_form" class="form-inline" action="{% url 'thread' slug=thread.slug, thread=thread.id, page=pagination['page'] %}" method="POST">
+    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+    <input type="hidden" name="origin" value="thread_form">
+    {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
+    <button type="submit" class="btn btn-primary">{% trans %}Go{% endtrans %}</button>
+  </form>
+</div>
+{% endif %}
+
 <div class="list-nav last">
   {{ pager(false) }}
   {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
@@ -197,3 +211,19 @@
 {% macro checkpoint_user(checkpoint) -%}
 {{ ('<a href="' ~ 'user'|url(user=checkpoint.user_id, username=checkpoint.user_slug) ~ '">')|safe ~ (checkpoint.user_name) ~ ("</a>")|safe }}
 {%- endmacro %}
+
+{% block javascripts -%}
+{{ super() }}
+{%- if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
+  <script type="text/javascript">
+    $(function () {
+      $('#thread_form').submit(function() {
+        if ($('#id_thread_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete this thread? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+    });
+  </script>{% endif %}
+{%- endblock %}