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):
 class ThreadsACL(BaseACL):
     def get_role(self, forum):
     def get_role(self, forum):
         try:
         try:
-            return self.acl[forum.pk]
+            try:
+                return self.acl[forum.pk]
+            except AttributeError:
+                return self.acl[forum]
         except KeyError:
         except KeyError:
             return {}
             return {}
     
     

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

@@ -1,23 +1,23 @@
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db.models import Q, F
 from django.db.models import Q, F
+from django import forms
 from django.forms import ValidationError
 from django.forms import ValidationError
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.acl.utils import ACLError403, ACLError404
-from misago.forms import FormLayout, FormFields
+from misago.forms import Form, FormLayout, FormFields
 from misago.forums.models import Forum
 from misago.forums.models import Forum
 from misago.messages import Message
 from misago.messages import Message
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
 from misago.threads.forms import MoveThreadsForm, MergeThreadsForm
 from misago.threads.forms import MoveThreadsForm, MergeThreadsForm
 from misago.threads.models import Thread, Post
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.base import BaseView
-from misago.threads.views.mixins import ThreadsFormMixin
 from misago.views import error403, error404
 from misago.views import error403, error404
 from misago.utils import make_pagination, slugify
 from misago.utils import make_pagination, slugify
 
 
-class ThreadsView(BaseView, ThreadsFormMixin):
+class ThreadsView(BaseView):
     def fetch_forum(self, forum):
     def fetch_forum(self, forum):
         self.forum = Forum.objects.get(pk=forum, type='forum')
         self.forum = Forum.objects.get(pk=forum, type='forum')
         self.proxy = Forum.objects.parents_aware_forum(self.forum)
         self.proxy = Forum.objects.parents_aware_forum(self.forum)
@@ -50,32 +50,89 @@ class ThreadsView(BaseView, ThreadsFormMixin):
         actions = []
         actions = []
         try:
         try:
             if acl['can_approve']:
             if acl['can_approve']:
-               actions.append(('accept', _('Accept threads')))
+                actions.append(('accept', _('Accept threads')))
             if acl['can_pin_threads'] == 2:
             if acl['can_pin_threads'] == 2:
-               actions.append(('annouce', _('Change to annoucements')))
+                actions.append(('annouce', _('Change to annoucements')))
             if acl['can_pin_threads'] > 0:
             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:
             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']:
             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']:
             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']:
             if acl['can_delete_threads']:
-               actions.append(('undelete', _('Undelete threads')))
+                actions.append(('undelete', _('Undelete threads')))
             if acl['can_delete_threads']:
             if acl['can_delete_threads']:
-               actions.append(('soft', _('Soft delete threads')))
+                actions.append(('soft', _('Soft delete threads')))
             if acl['can_delete_threads'] == 2:
             if acl['can_delete_threads'] == 2:
-               actions.append(('hard', _('Hard delete threads')))
+                actions.append(('hard', _('Hard delete threads')))
         except KeyError:
         except KeyError:
             pass
             pass
         return actions
         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):
     def action_accept(self, ids):
         accepted = 0
         accepted = 0
-        posts = 0
         users = []
         users = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.moderated:
             if thread.pk in ids and thread.moderated:
@@ -86,30 +143,18 @@ class ThreadsView(BaseView, ThreadsFormMixin):
                 thread.save(force_update=True)
                 thread.save(force_update=True)
                 thread.start_post.moderated = False
                 thread.start_post.moderated = False
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
+                thread.last_post.set_checkpoint(self.request, 'accepted')
                 # Sync user
                 # Sync user
                 if thread.last_post.user:
                 if thread.last_post.user:
                     thread.start_post.user.threads += 1
                     thread.start_post.user.threads += 1
                     thread.start_post.user.posts += 1
                     thread.start_post.user.posts += 1
                     users.append(thread.start_post.user)
                     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:
         if accepted:
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + 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)
             self.forum.save(force_update=True)
             for user in users:
             for user in users:
                 user.save(force_update=True)
                 user.save(force_update=True)
@@ -142,16 +187,6 @@ class ThreadsView(BaseView, ThreadsFormMixin):
         if normalised:
         if normalised:
             Thread.objects.filter(id__in=normalised).update(weight=0)
             Thread.objects.filter(id__in=normalised).update(weight=0)
             self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
             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):
     def action_move(self, ids):
         threads = []
         threads = []
@@ -234,7 +269,17 @@ class ThreadsView(BaseView, ThreadsFormMixin):
                                                       'threads': threads,
                                                       'threads': threads,
                                                       'form': FormLayout(form),
                                                       '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):
     def action_close(self, ids):
         closed = []
         closed = []
@@ -259,8 +304,7 @@ class ThreadsView(BaseView, ThreadsFormMixin):
         if undeleted:
         if undeleted:
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + len(undeleted)
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + len(undeleted)
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + posts
             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)
             self.forum.save(force_update=True)
             Thread.objects.filter(id__in=undeleted).update(deleted=False)
             Thread.objects.filter(id__in=undeleted).update(deleted=False)
             self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')
             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')
                             post.set_checkpoint(self.request, 'limit')
                 
                 
                 # Update last poster data
                 # Update last poster data
-                if not moderation:
+                if not moderation and self.mode not in ['edit_thread', 'edit_post']:
                     thread.last = now
                     thread.last = now
                     thread.last_post = post
                     thread.last_post = post
                     thread.last_poster = request.user
                     thread.last_poster = request.user

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

@@ -1,12 +1,15 @@
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
+from django import forms
+from django.forms import ValidationError
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.acl.utils import ACLError403, ACLError404
-from misago.forms import FormFields
+from misago.forms import Form, FormLayout, FormFields
 from misago.forums.models import Forum
 from misago.forums.models import Forum
+from misago.messages import Message
 from misago.readstracker.trackers import ThreadsTracker
 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.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
 from misago.views import error403, error404
@@ -40,7 +43,195 @@ class ThreadView(BaseView):
         if not self.tracker.is_read(self.thread):
         if not self.tracker.is_read(self.thread):
             self.tracker.set_read(self.thread, last_post)
             self.tracker.set_read(self.thread, last_post)
             self.tracker.sync()
             self.tracker.sync()
-     
+    
+    def 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):
     def __call__(self, request, slug=None, thread=None, page=0):
         self.request = request
         self.request = request
         self.pagination = None
         self.pagination = None
@@ -48,6 +239,11 @@ class ThreadView(BaseView):
         try:
         try:
             self.fetch_thread(thread)
             self.fetch_thread(thread)
             self.fetch_posts(page)
             self.fetch_posts(page)
+            self.make_thread_form()
+            if self.thread_form:
+                response = self.handle_thread_form()
+                if response:
+                    return response
         except Thread.DoesNotExist:
         except Thread.DoesNotExist:
             return error404(self.request)
             return error404(self.request)
         except ACLError403 as e:
         except ACLError403 as e:
@@ -66,6 +262,7 @@ class ThreadView(BaseView):
                                                  'count': self.count,
                                                  'count': self.count,
                                                  'posts': self.posts,
                                                  'posts': self.posts,
                                                  'pagination': self.pagination,
                                                  '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));
                                                 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 span.lead{color:#cccccc;}
 .posts-list .well-post .post-author .post-bit .user-title{color:#999999;}
 .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-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-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{clear:both;margin-left:286px;overflow:auto;padding:8px 16px;padding-bottom:0px;margin-bottom:-8px;}.posts-list .well-post .post-nav .changelog{float:left;opacity:0.5;filter:alpha(opacity=50);color:#999999;}
 .posts-list .well-post .post-nav ul{margin:0px;padding:0px;}
 .posts-list .well-post .post-nav ul{margin:0px;padding:0px;}
 .posts-list .well-post .post-nav .nav-pills li{opacity:0.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{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 .checkpoint{margin:0px;color:#999999;text-align:center;}.posts-list .post-checkpoints .checkpoint span{background-color:#fcfcfc;display:inline-block;padding:4px 12px;position:relative;bottom:16px;}
 .posts-list .post-checkpoints a{color:#333333;font-weight:bold;}
 .posts-list .post-checkpoints a{color:#333333;font-weight:bold;}
 .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 .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{margin-top:12px;overflow:auto;}.quick-reply .avatar-big,.quick-reply .arrow{float:left;}
 .quick-reply .arrow{width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-right:12px solid #e3e3e3;position:relative;top:12px;left:5px;}
 .quick-reply .arrow{width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-right:12px solid #e3e3e3;position:relative;top:12px;left:5px;}
 .quick-reply .editor{margin-left:142px;}
 .quick-reply .editor{margin-left:142px;}

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

@@ -160,30 +160,9 @@
 
 
     .post-content {
     .post-content {
       margin-left: 286px;
       margin-left: 286px;
+      margin-right: 200px;
       padding: 0px 16px;
       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 {
       .post-foot {
         margin-top: 20px;
         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 {
     .post-nav {
       clear: both;
       clear: both;
       margin-left: 286px;
       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 box
 .quick-reply {
 .quick-reply {
   margin-top: 12px;
   margin-top: 12px;

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

@@ -4,14 +4,23 @@
 {% import "_forms.html" as form_theme with context %}
 {% import "_forms.html" as form_theme with context %}
 {% import "sora/macros.html" as macros with context %}
 {% import "sora/macros.html" as macros with context %}
 
 
-{% block title %}{{ macros.page_title(title=_("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>
 {% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
 {% for parent in parents %}
 {% 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>
 <li class="first"><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
 {% endfor %}
 {% 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 %}
 <li class="active">{% trans %}Move Threads{% endtrans %}
+{% endif %}
 {%- endblock %}
 {%- endblock %}
 
 
 {% block content %}
 {% block content %}
@@ -19,22 +28,40 @@
   <ul class="breadcrumb">
   <ul class="breadcrumb">
     {{ self.breadcrumb() }}</li>
     {{ self.breadcrumb() }}</li>
   </ul>
   </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>
   <h1>{% trans %}Move Threads{% endtrans %} <small>{{ forum.name }}</small></h1>
+  {% endif %}
 </div>
 </div>
 <div class="row">
 <div class="row">
   <div class="span8 offset2">
   <div class="span8 offset2">
     {% if message %}{{ macros.draw_message(message) }}{% endif %}
     {% 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 }}">
       <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="origin" value="move_form">
       <input type="hidden" name="list_action" value="move">
       <input type="hidden" name="list_action" value="move">
       {% for thread in threads -%}
       {% for thread in threads -%}
       <input type="hidden" name="list_items" value="{{ thread.pk }}">
       <input type="hidden" name="list_items" value="{{ thread.pk }}">
       {% endfor %}
       {% endfor %}
+      {% endif %}
       {{ form_theme.form_widget(form, width=8) }}
       {{ form_theme.form_widget(form, width=8) }}
       <div class="form-actions">
       <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>
       </div>
     </form>
     </form>
   </div>
   </div>

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

@@ -1,6 +1,7 @@
 {% extends "sora/layout.html" %}
 {% extends "sora/layout.html" %}
 {% load i18n %}
 {% load i18n %}
 {% load url from future %}
 {% load url from future %}
+{% import "_forms.html" as form_theme with context %}
 {% import "sora/editor.html" as editor with context %}
 {% import "sora/editor.html" as editor with context %}
 {% import "sora/macros.html" as macros with context %}
 {% import "sora/macros.html" as macros with context %}
 
 
@@ -58,34 +59,34 @@
         <p class="post-date">{{ post.date|reltimesince|low }}</p>
         <p class="post-date">{{ post.date|reltimesince|low }}</p>
       </div>
       </div>
     </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-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">
       <div class="markdown">
         {{ post.post_preparsed|safe }}
         {{ post.post_preparsed|safe }}
       </div>
       </div>
@@ -112,6 +113,8 @@
       {% endif %}
       {% endif %}
       {% if user.is_authenticated() %}
       {% if user.is_authenticated() %}
       <ul class="nav nav-pills pull-right">
       <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 %}
         {% if 1 == 2 %}
         <li><a href="#"><i class="icon-info-sign"></i> {% trans %}Info{% endtrans %}</a></li>
         <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><button class="btn danger"><i class="icon-remove"></i> {% trans %}Delete{% endtrans %}</button></li>
@@ -153,6 +156,17 @@
   {% endfor %}
   {% endfor %}
 </div>
 </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">
 <div class="list-nav last">
   {{ pager(false) }}
   {{ pager(false) }}
   {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
   {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
@@ -197,3 +211,19 @@
 {% macro checkpoint_user(checkpoint) -%}
 {% macro checkpoint_user(checkpoint) -%}
 {{ ('<a href="' ~ 'user'|url(user=checkpoint.user_id, username=checkpoint.user_slug) ~ '">')|safe ~ (checkpoint.user_name) ~ ("</a>")|safe }}
 {{ ('<a href="' ~ 'user'|url(user=checkpoint.user_id, username=checkpoint.user_slug) ~ '">')|safe ~ (checkpoint.user_name) ~ ("</a>")|safe }}
 {%- endmacro %}
 {%- 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 %}