Browse Source

#410: open/close threads

Rafał Pitoń 10 years ago
parent
commit
c599ca1257

+ 30 - 0
misago/threads/permissions.py

@@ -92,6 +92,10 @@ class PermissionsForm(forms.Form):
         label=_("Can protect posts"),
         help_text=_("Only users with this permission "
                     "can edit protected posts."))
+    can_move_posts = forms.YesNoSwitch(
+        label=_("Can move posts"))
+    can_merge_posts = forms.YesNoSwitch(
+        label=_("Can merge posts"))
     can_change_threads_labels = forms.TypedChoiceField(
         label=_("Can change threads labels"), coerce=int, initial=0,
         choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads"))))
@@ -107,6 +111,12 @@ class PermissionsForm(forms.Form):
         coerce=int,
         initial=0,
         choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads"))))
+    can_move_threads = forms.YesNoSwitch(
+        label=_("Can move threads"))
+    can_merge_threads = forms.YesNoSwitch(
+        label=_("Can merge threads"))
+    can_split_threads = forms.YesNoSwitch(
+        label=_("Can split threads"))
     can_review_moderated_content = forms.YesNoSwitch(
         label=_("Can review moderated content"),
         help_text=_("Will see and be able to accept moderated content."))
@@ -161,9 +171,14 @@ def build_forum_acl(acl, forum, forums_roles, key_name):
         'can_hide_threads': 0,
         'can_hide_replies': 0,
         'can_protect_posts': 0,
+        'can_move_posts': 0,
+        'can_merge_posts': 0,
         'can_change_threads_labels': 0,
         'can_change_threads_weight': 0,
         'can_close_threads': 0,
+        'can_move_threads': 0,
+        'can_merge_threads': 0,
+        'can_split_threads': 0,
         'can_review_moderated_content': 0,
         'can_report_content': 0,
         'can_see_reports': 0,
@@ -184,9 +199,14 @@ def build_forum_acl(acl, forum, forums_roles, key_name):
         thread_edit_time=algebra.greater_or_zero,
         reply_edit_time=algebra.greater_or_zero,
         can_protect_posts=algebra.greater,
+        can_move_posts=algebra.greater,
+        can_merge_posts=algebra.greater,
         can_change_threads_labels=algebra.greater,
         can_change_threads_weight=algebra.greater,
         can_close_threads=algebra.greater,
+        can_move_threads=algebra.greater,
+        can_merge_threads=algebra.greater,
+        can_split_threads=algebra.greater,
         can_review_moderated_content=algebra.greater,
         can_report_content=algebra.greater,
         can_see_reports=algebra.greater,
@@ -226,9 +246,14 @@ def add_acl_to_forum(user, forum):
         'can_hide_threads': 0,
         'can_hide_replies': 0,
         'can_protect_posts': 0,
+        'can_move_posts': 0,
+        'can_merge_posts': 0,
         'can_change_threads_labels': 0,
         'can_change_threads_weight': 0,
         'can_close_threads': 0,
+        'can_move_threads': 0,
+        'can_merge_threads': 0,
+        'can_split_threads': 0,
         'can_review_moderated_content': 0,
         'can_report_content': 0,
         'can_see_reports': 0,
@@ -251,9 +276,14 @@ def add_acl_to_forum(user, forum):
             thread_edit_time=algebra.greater_or_zero,
             reply_edit_time=algebra.greater_or_zero,
             can_protect_posts=algebra.greater,
+            can_move_posts=algebra.greater,
+            can_merge_posts=algebra.greater,
             can_change_threads_labels=algebra.greater,
             can_change_threads_weight=algebra.greater,
             can_close_threads=algebra.greater,
+            can_move_threads=algebra.greater,
+            can_merge_threads=algebra.greater,
+            can_split_threads=algebra.greater,
             can_review_moderated_content=algebra.greater,
             can_report_content=algebra.greater,
             can_see_reports=algebra.greater,

+ 92 - 0
misago/threads/tests/test_forumthreads_view.py

@@ -97,6 +97,33 @@ class ActionsTests(ForumViewHelperTestCase):
             },
         ])
 
+    def test_close_open_actions(self):
+        """ForumActions initializes valid list of close and open"""
+        self.override_acl({
+            'can_close_threads': [],
+        })
+
+        actions = ForumActions(user=self.user, forum=self.forum)
+        self.assertEqual(actions.available_actions, [])
+
+        self.override_acl({
+            'can_close_threads': 1,
+        })
+
+        actions = ForumActions(user=self.user, forum=self.forum)
+        self.assertEqual(actions.available_actions, [
+            {
+                'action': 'close',
+                'icon': 'lock',
+                'name': _("Close threads")
+            },
+            {
+                'action': 'open',
+                'icon': 'unlock-alt',
+                'name': _("Open threads")
+            },
+        ])
+
 
 class ForumFilteringTests(ForumViewHelperTestCase):
     def test_get_available_filters(self):
@@ -684,3 +711,68 @@ class ForumThreadsViewTests(AuthenticatedUserTestCase):
         self.assertEqual(announcement.weight, 2)
         self.assertEqual(pinned.weight, 1)
         self.assertEqual(thread.weight, 0)
+
+    def test_close_open_threads(self):
+        """moderation allows for closing and opening threads"""
+        test_acl = {
+            'can_see': 1,
+            'can_browse': 1,
+            'can_see_all_threads': 1,
+            'can_close_threads': 1
+        }
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("Close threads", response.content)
+        self.assertIn("Open threads", response.content)
+
+        threads = [testutils.post_thread(self.forum) for t in xrange(10)]
+
+        # close threads
+        self.override_acl(test_acl)
+        response = self.client.post(self.link, data={
+            'action': 'close', 'thread': [t.pk for t in threads]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("10 threads were closed.", response.content)
+
+        # close closed threads
+        self.override_acl(test_acl)
+        response = self.client.post(self.link, data={
+            'action': 'close', 'thread': [t.pk for t in threads]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("No threads were closed.", response.content)
+
+        # open closed threads
+        self.override_acl(test_acl)
+        response = self.client.post(self.link, data={
+            'action': 'open', 'thread': [t.pk for t in threads]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("10 threads were opened.", response.content)
+
+        # open opened threads
+        self.override_acl(test_acl)
+        response = self.client.post(self.link, data={
+            'action': 'open', 'thread': [t.pk for t in threads]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("No threads were opened.", response.content)

+ 42 - 0
misago/threads/tests/test_threads_moderation.py

@@ -77,3 +77,45 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
         """reset_thread returns false for already default thread"""
         self.assertFalse(moderation.reset_thread(self.user, self.thread))
         self.assertEqual(self.thread.weight, 0)
+
+    def test_close_thread(self):
+        """close_thread closes thread"""
+        self.assertFalse(self.thread.is_closed)
+        self.assertTrue(moderation.close_thread(self.user, self.thread))
+
+        self.reload_thread()
+        self.assertTrue(self.thread.is_closed)
+        self.assertTrue(self.thread.has_events)
+        event = self.thread.event_set.last()
+
+        self.assertIn("closed thread.", event.message)
+        self.assertEqual(event.icon, "lock")
+
+    def test_close_invalid_thread(self):
+        """close_thread fails gracefully for opened thread"""
+        moderation.close_thread(self.user, self.thread)
+        self.reload_thread()
+
+        self.assertTrue(self.thread.is_closed)
+        self.assertFalse(moderation.close_thread(self.user, self.thread))
+
+    def test_open_thread(self):
+        """open_thread closes thread"""
+        moderation.close_thread(self.user, self.thread)
+        self.reload_thread()
+
+        self.assertTrue(self.thread.is_closed)
+        self.assertTrue(moderation.open_thread(self.user, self.thread))
+
+        self.reload_thread()
+        self.assertFalse(self.thread.is_closed)
+        self.assertTrue(self.thread.has_events)
+        event = self.thread.event_set.last()
+
+        self.assertIn("opened thread.", event.message)
+        self.assertEqual(event.icon, "unlock-alt")
+
+    def test_open_invalid_thread(self):
+        """open_thread fails gracefully for opened thread"""
+        self.assertFalse(self.thread.is_closed)
+        self.assertFalse(moderation.open_thread(self.user, self.thread))

+ 44 - 0
misago/threads/views/generic/forum.py

@@ -39,6 +39,18 @@ class ForumActions(Actions):
                 'icon': 'circle',
                 'name': _("Reset weight")
             })
+        if forum.acl['can_close_threads']:
+            actions.append({
+                'action': 'close',
+                'icon': 'lock',
+                'name': _("Close threads")
+            })
+            actions.append({
+                'action': 'open',
+                'icon': 'unlock-alt',
+                'name': _("Open threads")
+            })
+
         return actions
 
     def action_announce(self, request, threads):
@@ -89,6 +101,38 @@ class ForumActions(Actions):
             message = ("No threads weight was reset.")
             messages.info(request, message)
 
+    def action_close(self, request, threads):
+        changed_threads = 0
+        for thread in threads:
+            if moderation.close_thread(request.user, thread):
+                changed_threads += 1
+
+        if changed_threads:
+            message = ungettext(
+                '%(changed)d thread was closed.',
+                '%(changed)d threads were closed.',
+            changed_threads)
+            messages.success(request, message % {'changed': changed_threads})
+        else:
+            message = ("No threads were closed.")
+            messages.info(request, message)
+
+    def action_open(self, request, threads):
+        changed_threads = 0
+        for thread in threads:
+            if moderation.open_thread(request.user, thread):
+                changed_threads += 1
+
+        if changed_threads:
+            message = ungettext(
+                '%(changed)d thread was opened.',
+                '%(changed)d threads were opened.',
+            changed_threads)
+            messages.success(request, message % {'changed': changed_threads})
+        else:
+            message = ("No threads were opened.")
+            messages.info(request, message)
+
 
 class ForumFiltering(object):
     def __init__(self, forum, link_name, link_params):