Просмотр исходного кода

Threads and announcements lists refactored.

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

+ 22 - 3
misago/apps/announcements/views.py

@@ -1,10 +1,11 @@
-from misago.apps.forumbase.list import ThreadsListBaseView
+from django.utils.translation import ugettext as _
+from misago.apps.forumbase.list import ThreadsListBaseView, ThreadsListModeration
 from misago.models import Forum, Thread
 from misago.models import Forum, Thread
 from misago.readstrackers import ThreadsTracker
 from misago.readstrackers import ThreadsTracker
 from misago.utils.pagination import make_pagination
 from misago.utils.pagination import make_pagination
 from misago.apps.announcements.mixins import TypeMixin
 from misago.apps.announcements.mixins import TypeMixin
 
 
-class ThreadsListView(ThreadsListBaseView, TypeMixin):
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
     def fetch_forum(self):
     def fetch_forum(self):
         self.forum = Forum.objects.get(special='announcements')
         self.forum = Forum.objects.get(special='announcements')
 
 
@@ -19,4 +20,22 @@ class ThreadsListView(ThreadsListBaseView, TypeMixin):
         tracker = ThreadsTracker(self.request, self.forum)
         tracker = ThreadsTracker(self.request, self.forum)
         for thread in queryset[self.pagination['start']:self.pagination['stop']]:
         for thread in queryset[self.pagination['start']:self.pagination['stop']]:
             thread.is_read = tracker.is_read(thread)
             thread.is_read = tracker.is_read(thread)
-            self.threads.append(thread)
+            self.threads.append(thread)
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            if acl['can_approve']:
+                actions.append(('accept', _('Accept threads')))
+            if acl['can_close_threads']:
+                actions.append(('open', _('Open threads')))
+                actions.append(('close', _('Close threads')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Undelete threads')))
+                actions.append(('soft', _('Soft delete threads')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Hard delete threads')))
+        except KeyError:
+            pass
+        return actions

+ 0 - 58
misago/apps/forumbase/list.py

@@ -1,58 +0,0 @@
-from django.template import RequestContext
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.errors import error403, error404
-from misago.forms import FormFields
-from misago.models import Forum
-from misago.readstrackers import ForumsTracker
-
-class ThreadsListBaseView(object):
-    def _fetch_forum(self):
-        self.fetch_forum()
-        self.proxy = Forum.objects.parents_aware_forum(self.forum)
-        self.request.acl.forums.allow_forum_view(self.forum)
-        if self.forum.level:
-            self.parents = Forum.objects.forum_parents(self.forum.pk)
-        if self.forum.lft + 1 != self.forum.rght:
-            self.forum.subforums = Forum.objects.treelist(self.request.acl.forums, self.forum, tracker=ForumsTracker(self.request.user))
-    
-    def __new__(cls, request, **kwargs):
-        obj = super(ThreadsListBaseView, cls).__new__(cls)
-        return obj(request, **kwargs)
-
-    def __call__(self, request, **kwargs):
-        self.request = request
-        self.kwargs = kwargs
-        self.pagination = {}
-        self.parents = []
-        self.threads = []
-        self.message = request.messages.get_message('threads')
-        try:
-            self._fetch_forum()
-            self.fetch_threads()
-            self.form = None
-            #self.make_form()
-            #if self.form:
-            #    response = self.handle_form()
-            #    if response:
-            #        return response
-        except Forum.DoesNotExist:
-            return error404(request)
-        except ACLError403 as e:
-            return error403(request, unicode(e))
-        except ACLError404 as e:
-            return error404(request, unicode(e))
-
-        # Merge proxy into forum
-        self.forum.closed = self.proxy.closed
-        
-        return request.theme.render_to_response(('%s/list.html' % self.templates_prefix),
-                                                {
-                                                 'message': self.message,
-                                                 'forum': self.forum,
-                                                 'parents': self.parents,
-                                                 'count': self.count,
-                                                 'list_form': FormFields(self.form).fields if self.form else None,
-                                                 'threads': self.threads,
-                                                 'pagination': self.pagination,
-                                                 },
-                                                context_instance=RequestContext(request));

+ 2 - 0
misago/apps/forumbase/list/__init__.py

@@ -0,0 +1,2 @@
+from misago.apps.forumbase.list.views import ThreadsListBaseView
+from misago.apps.forumbase.list.moderation import ThreadsListModeration

+ 89 - 0
misago/apps/forumbase/list/forms.py

@@ -0,0 +1,89 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from misago.forms import Form, ForumChoiceField
+from misago.models import Forum
+from misago.validators import validate_sluggable
+from misago.apps.forumbase.mixins import ValidateThreadNameMixin
+
+class MoveThreadsForm(Form):
+    error_source = 'new_forum'
+
+    def __init__(self, data=None, request=None, forum=None, *args, **kwargs):
+        self.forum = forum
+        super(MoveThreadsForm, self).__init__(data, request=request, *args, **kwargs)
+
+    def finalize_form(self):
+        self.fields['new_forum'] = ForumChoiceField(queryset=Forum.tree.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']))
+        self.layout = [
+                       [
+                        None,
+                        [
+                         ('new_forum', {'label': _("Move Threads to"), 'help_text': _("Select forum you want to move threads to.")}),
+                         ],
+                        ],
+                       ]
+
+    def clean_new_forum(self):
+        new_forum = self.cleaned_data['new_forum']
+        # Assert its forum and its not current forum
+        if new_forum.type != 'forum':
+            raise forms.ValidationError(_("This is not forum."))
+        if new_forum.pk == self.forum.pk:
+            raise forms.ValidationError(_("New forum is same as current one."))
+        return new_forum
+
+
+class MergeThreadsForm(Form, ValidateThreadNameMixin):
+    def __init__(self, data=None, request=None, threads=[], *args, **kwargs):
+        self.threads = threads
+        super(MergeThreadsForm, self).__init__(data, request=request, *args, **kwargs)
+
+    def finalize_form(self):
+        self.fields['new_forum'] = ForumChoiceField(queryset=Forum.tree.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']), initial=self.threads[0].forum)
+        self.fields['thread_name'] = forms.CharField(
+                                                     max_length=self.request.settings['thread_name_max'],
+                                                     initial=self.threads[0].name,
+                                                     validators=[validate_sluggable(
+                                                                                    _("Thread name must contain at least one alpha-numeric character."),
+                                                                                    _("Thread name is too long. Try shorter name.")
+                                                                                    )])
+        self.layout = [
+                       [
+                        None,
+                        [
+                         ('thread_name', {'label': _("Thread Name"), 'help_text': _("Name of new thread that will be created as result of merge.")}),
+                         ('new_forum', {'label': _("Thread Forum"), 'help_text': _("Select forum you want to put new thread in.")}),
+                         ],
+                        ],
+                       [
+                        _("Merge Order"),
+                        [
+                         ],
+                        ],
+                       ]
+
+        choices = []
+        for i, thread in enumerate(self.threads):
+            choices.append((str(i), i + 1))
+        for i, thread in enumerate(self.threads):
+            self.fields['thread_%s' % thread.pk] = forms.ChoiceField(choices=choices, initial=str(i))
+            self.layout[1][1].append(('thread_%s' % thread.pk, {'label': thread.name}))
+
+    def clean_new_forum(self):
+        new_forum = self.cleaned_data['new_forum']
+        # Assert its forum
+        if new_forum.type != 'forum':
+            raise forms.ValidationError(_("This is not forum."))
+        return new_forum
+
+    def clean(self):
+        cleaned_data = super(MergeThreadsForm, self).clean()
+        self.merge_order = {}
+        lookback = []
+        for thread in self.threads:
+            order = int(cleaned_data['thread_%s' % thread.pk])
+            if order in lookback:
+                raise forms.ValidationError(_("One or more threads have same position in merge order."))
+            lookback.append(order)
+            self.merge_order[order] = thread
+        return cleaned_data

+ 230 - 0
misago/apps/forumbase/list/moderation.py

@@ -0,0 +1,230 @@
+from django.forms import ValidationError
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.forms import FormLayout
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.apps.forumbase.list.forms import MoveThreadsForm, MergeThreadsForm
+
+class ThreadsListModeration(object):
+    def action_accept(self, ids):
+        accepted = 0
+        last_posts = []
+        users = []
+        for thread in self.threads:
+            if thread.pk in ids and thread.moderated:
+                accepted += 1
+                # Sync thread and post
+                thread.moderated = False
+                thread.replies_moderated -= 1
+                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')
+                last_posts.append(thread.last_post.pk)
+                # 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)
+        if accepted:
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
+            self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
+            self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            for user in users:
+                user.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
+
+    def action_annouce(self, ids):
+        acl = self.request.acl.threads.get_role(self.forum)
+        annouced = []
+        for thread in self.threads:
+            if thread.pk in ids and thread.weight < 2:
+                annouced.append(thread.pk)
+        if annouced:
+            Thread.objects.filter(id__in=annouced).update(weight=2)
+            self.request.messages.set_flash(Message(_('Selected threads have been turned into announcements.')), 'success', 'threads')
+
+    def action_sticky(self, ids):
+        acl = self.request.acl.threads.get_role(self.forum)
+        sticky = []
+        for thread in self.threads:
+            if thread.pk in ids and thread.weight != 1 and (acl['can_pin_threads'] == 2 or thread.weight < 2):
+                sticky.append(thread.pk)
+        if sticky:
+            Thread.objects.filter(id__in=sticky).update(weight=1)
+            self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
+
+    def action_normal(self, ids):
+        normalised = []
+        for thread in self.threads:
+            if thread.pk in ids and thread.weight > 0:
+                normalised.append(thread.pk)
+        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_move(self, ids):
+        threads = []
+        for thread in self.threads:
+            if thread.pk in ids:
+                threads.append(thread)
+        if self.request.POST.get('origin') == 'move_form':
+            form = MoveThreadsForm(self.request.POST, request=self.request, forum=self.forum)
+            if form.is_valid():
+                new_forum = form.cleaned_data['new_forum']
+                for thread in threads:
+                    thread.move_to(new_forum)
+                    thread.save(force_update=True)
+                new_forum.sync()
+                new_forum.save(force_update=True)
+                self.forum.sync()
+                self.forum.save(force_update=True)
+                self.request.messages.set_flash(Message(_('Selected threads have been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
+                return None
+            self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = MoveThreadsForm(request=self.request, forum=self.forum)
+        return self.request.theme.render_to_response(('%s/move_threads.html' % self.templates_prefix),
+                                                     {
+                                                      'message': self.message,
+                                                      'forum': self.forum,
+                                                      'parents': self.parents,
+                                                      'threads': threads,
+                                                      'form': FormLayout(form),
+                                                      },
+                                                     context_instance=RequestContext(self.request));
+
+    def action_merge(self, ids):
+        if len(ids) < 2:
+            raise ValidationError(_("You have to pick two or more threads to merge."))
+        threads = []
+        for thread in self.threads:
+            if thread.pk in ids:
+                threads.append(thread)
+        if self.request.POST.get('origin') == 'merge_form':
+            form = MergeThreadsForm(self.request.POST, request=self.request, threads=threads)
+            if form.is_valid():
+                new_thread = Thread.objects.create(
+                                                   forum=form.cleaned_data['new_forum'],
+                                                   name=form.cleaned_data['thread_name'],
+                                                   slug=slugify(form.cleaned_data['thread_name']),
+                                                   start=timezone.now(),
+                                                   last=timezone.now()
+                                                   )
+                last_merge = 0
+                last_thread = None
+                merged = []
+                for i in range(0, len(threads)):
+                    thread = form.merge_order[i]
+                    merged.append(thread.pk)
+                    if last_thread and last_thread.last > thread.start:
+                        last_merge += thread.merges + 1
+                    thread.merge_with(new_thread, last_merge=last_merge)
+                    last_thread = thread
+                Thread.objects.filter(id__in=merged).delete()
+                new_thread.sync()
+                new_thread.save(force_update=True)
+                self.forum.sync()
+                self.forum.save(force_update=True)
+                if form.cleaned_data['new_forum'].pk != self.forum.pk:
+                    form.cleaned_data['new_forum'].sync()
+                    form.cleaned_data['new_forum'].save(force_update=True)
+                self.request.messages.set_flash(Message(_('Selected threads have been merged into new one.')), 'success', 'threads')
+                return None
+            self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = MergeThreadsForm(request=self.request, threads=threads)
+        return self.request.theme.render_to_response(('%s/merge.html' % self.templates_prefix),
+                                                     {
+                                                      'message': self.message,
+                                                      'forum': self.forum,
+                                                      'parents': self.parents,
+                                                      'threads': threads,
+                                                      'form': FormLayout(form),
+                                                      },
+                                                     context_instance=RequestContext(self.request));
+
+    def action_open(self, ids):
+        opened = []
+        last_posts = []
+        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')
+                last_posts.append(thread.last_post.pk)
+        if opened:
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
+            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 = []
+        last_posts = []
+        for thread in self.threads:
+            if thread.pk in ids and not thread.closed:
+                closed.append(thread.pk)
+                thread.last_post.set_checkpoint(self.request, 'closed')
+                last_posts.append(thread.last_post.pk)
+        if closed:
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
+            Thread.objects.filter(id__in=closed).update(closed=True)
+            self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
+
+    def action_undelete(self, ids):
+        undeleted = []
+        last_posts = []
+        posts = 0
+        for thread in self.threads:
+            if thread.pk in ids and thread.deleted:
+                undeleted.append(thread.pk)
+                posts += thread.replies + 1
+                thread.start_post.deleted = False
+                thread.start_post.save(force_update=True)
+                thread.last_post.set_checkpoint(self.request, 'undeleted')
+        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.sync()
+            self.forum.save(force_update=True)
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
+            Thread.objects.filter(id__in=undeleted).update(deleted=False)
+            self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')
+
+    def action_soft(self, ids):
+        deleted = []
+        last_posts = []
+        posts = 0
+        for thread in self.threads:
+            if thread.pk in ids and not thread.deleted:
+                deleted.append(thread.pk)
+                posts += thread.replies + 1
+                thread.start_post.deleted = True
+                thread.start_post.save(force_update=True)
+                thread.last_post.set_checkpoint(self.request, 'deleted')
+                last_posts.append(thread.last_post.pk)
+        if deleted:
+            self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
+            self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
+            Thread.objects.filter(id__in=deleted).update(deleted=True)
+            self.request.messages.set_flash(Message(_('Selected threads have been softly deleted.')), 'success', 'threads')
+
+    def action_hard(self, ids):
+        deleted = []
+        posts = 0
+        for thread in self.threads:
+            if thread.pk in ids:
+                deleted.append(thread.pk)
+                posts += thread.replies + 1
+                thread.delete()
+        if deleted:
+            self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
+            self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')

+ 123 - 0
misago/apps/forumbase/list/views.py

@@ -0,0 +1,123 @@
+from django import forms
+from django.forms import ValidationError
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.forms import Form, FormFields
+from misago.models import Forum, Thread, Post
+from misago.readstrackers import ForumsTracker
+
+class ThreadsListBaseView(object):
+    def _fetch_forum(self):
+        self.fetch_forum()
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        if self.forum.level:
+            self.parents = Forum.objects.forum_parents(self.forum.pk)
+        if self.forum.lft + 1 != self.forum.rght:
+            self.forum.subforums = Forum.objects.treelist(self.request.acl.forums, self.forum, tracker=ForumsTracker(self.request.user))
+
+    def threads_actions(self):
+        pass
+
+    def make_form(self):
+        self.form = None
+        list_choices = self.threads_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 __new__(cls, request, **kwargs):
+        obj = super(ThreadsListBaseView, cls).__new__(cls)
+        return obj(request, **kwargs)
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.pagination = {}
+        self.parents = []
+        self.threads = []
+        self.message = request.messages.get_message('threads')
+        try:
+            self._fetch_forum()
+            self.fetch_threads()
+            self.form = None
+            self.make_form()
+            if self.form:
+                response = self.handle_form()
+                if response:
+                    return response
+        except Forum.DoesNotExist:
+            return error404(request)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+        # Merge proxy into forum
+        self.forum.closed = self.proxy.closed
+
+        return request.theme.render_to_response(('%s/list.html' % self.templates_prefix),
+                                                {
+                                                 'message': self.message,
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'count': self.count,
+                                                 'list_form': FormFields(self.form).fields if self.form else None,
+                                                 'threads': self.threads,
+                                                 'pagination': self.pagination,
+                                                 },
+                                                context_instance=RequestContext(request));

+ 21 - 0
misago/apps/forumbase/mixins.py

@@ -0,0 +1,21 @@
+from django import forms
+from django.utils.translation import ungettext
+from misago.utils.strings import slugify
+
+class ValidateThreadNameMixin(object):
+    def clean_thread_name(self):
+        data = self.cleaned_data['thread_name']
+        slug = slugify(data)
+        if len(slug) < self.request.settings['thread_name_min']:
+            raise forms.ValidationError(ungettext(
+                                                  "Thread name must contain at least one alpha-numeric character.",
+                                                  "Thread name must contain at least %(count)d alpha-numeric characters.",
+                                                  self.request.settings['thread_name_min']
+                                                  ) % {'count': self.request.settings['thread_name_min']})
+        if len(data) > self.request.settings['thread_name_max']:
+            raise forms.ValidationError(ungettext(
+                                                  "Thread name cannot be longer than %(count)d character.",
+                                                  "Thread name cannot be longer than %(count)d characters.",
+                                                  self.request.settings['thread_name_max']
+                                                  ) % {'count': self.request.settings['thread_name_max']})
+        return data

+ 31 - 3
misago/apps/threads/views.py

@@ -1,11 +1,12 @@
+from django.utils.translation import ugettext as _
 from itertools import chain
 from itertools import chain
-from misago.apps.forumbase.list import ThreadsListBaseView
+from misago.apps.forumbase.list import ThreadsListBaseView, ThreadsListModeration
 from misago.models import Forum, Thread
 from misago.models import Forum, Thread
 from misago.readstrackers import ThreadsTracker
 from misago.readstrackers import ThreadsTracker
 from misago.utils.pagination import make_pagination
 from misago.utils.pagination import make_pagination
 from misago.apps.threads.mixins import TypeMixin
 from misago.apps.threads.mixins import TypeMixin
 
 
-class ThreadsListView(ThreadsListBaseView, TypeMixin):
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
     def fetch_forum(self):
     def fetch_forum(self):
         self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
         self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
 
 
@@ -43,4 +44,31 @@ class ThreadsListView(ThreadsListBaseView, TypeMixin):
             else:
             else:
                 thread.weight = 2
                 thread.weight = 2
                 thread.is_read = tracker_annos.is_read(thread)
                 thread.is_read = tracker_annos.is_read(thread)
-            self.threads.append(thread)
+            self.threads.append(thread)
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            if acl['can_approve']:
+                actions.append(('accept', _('Accept threads')))
+            if acl['can_pin_threads'] == 2:
+                actions.append(('annouce', _('Change to announcements')))
+            if acl['can_pin_threads'] > 0:
+                actions.append(('sticky', _('Change to sticky threads')))
+            if acl['can_pin_threads'] > 0:
+                actions.append(('normal', _('Change to standard thread')))
+            if acl['can_move_threads_posts']:
+                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')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Undelete threads')))
+                actions.append(('soft', _('Soft delete threads')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Hard delete threads')))
+        except KeyError:
+            pass
+        return actions

+ 57 - 0
templates/cranefly/announcements/merge.html

@@ -0,0 +1,57 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_("Merge Announcements"),parent=_('Global Announcements')) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'announcements' %}">{% trans %}Global Announcements{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans %}Merge Announcements{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans %}Merge Announcements{% endtrans %} <small>{% trans %}Global Announcements{% endtrans %}</small></h1>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <div class="row">
+    <div class="span6 offset3">
+      <div class="form-container">
+
+        <div class="form-header">
+          <h1>{% trans %}Merge Announcements{% endtrans %}</h1>
+        </div>
+
+        {% if message %}
+        <div class="messages-list">
+          {{ macros.draw_message(message) }}
+        </div>
+        {% endif %}
+
+        <form action="{% url 'announcements' %}" method="post">
+          <input type="hidden" name="origin" value="merge_form">
+          <input type="hidden" name="list_action" value="merge">
+          {% for thread in threads -%}
+          <input type="hidden" name="list_items" value="{{ thread.pk }}">
+          {% endfor %}
+          <div class="form-fields">
+            {{ form_theme.form_widget(form, width=6) }}
+          </div>
+          <div class="form-actions">
+            <button type="submit" class="btn btn-primary">{% trans %}Merge Threads{% endtrans %}</button>
+            <a href="{% url 'announcements' %}" class="btn">{% trans %}Cancel{% endtrans %}</a>
+          </div>
+        </form>
+
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock %}