Browse Source

Improvements in threads moderation

Rafał Pitoń 10 years ago
parent
commit
4cfa7e7294

+ 3 - 0
misago/forums/models.py

@@ -91,6 +91,9 @@ class Forum(MPTTModel):
         else:
         else:
             return self.name
             return self.name
 
 
+    def lock(self):
+        return Forum.objects.select_for_update().get(id=self.id)
+
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
         Forum.objects.clear_cache()
         Forum.objects.clear_cache()
         acl_version.invalidate()
         acl_version.invalidate()

+ 3 - 0
misago/threads/models/thread.py

@@ -57,6 +57,9 @@ class Thread(models.Model):
     def __unicode__(self):
     def __unicode__(self):
         return self.title
         return self.title
 
 
+    def lock(self):
+        return Forum.objects.select_for_update().get(id=self.id)
+
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
         from misago.threads.signals import delete_thread
         from misago.threads.signals import delete_thread
         delete_thread.send(sender=self)
         delete_thread.send(sender=self)

+ 55 - 8
misago/threads/permissions.py

@@ -1,6 +1,7 @@
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from django.db.models import Q
 from django.db.models import Q
 from django.http import Http404
 from django.http import Http404
+from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.acl import add_acl, algebra
 from misago.acl import add_acl, algebra
@@ -101,11 +102,7 @@ class PermissionsForm(forms.Form):
         choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads"))))
         choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads"))))
     can_pin_threads = forms.YesNoSwitch(
     can_pin_threads = forms.YesNoSwitch(
         label=_("Can pin threads"))
         label=_("Can pin threads"))
-    can_close_threads = forms.TypedChoiceField(
-        label=_("Can close threads"),
-        coerce=int,
-        initial=0,
-        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads"))))
+    can_close_threads = forms.YesNoSwitch(label=_("Can close threads"))
     can_move_threads = forms.YesNoSwitch(
     can_move_threads = forms.YesNoSwitch(
         label=_("Can move threads"))
         label=_("Can move threads"))
     can_merge_threads = forms.YesNoSwitch(
     can_merge_threads = forms.YesNoSwitch(
@@ -289,7 +286,24 @@ def add_acl_to_forum(user, forum):
 
 
 
 
 def add_acl_to_thread(user, thread):
 def add_acl_to_thread(user, thread):
-    pass
+    forum_acl = user.acl['forums'].get(thread.forum_id, {})
+
+    thread.acl.update({
+        'can_reply': 0,
+        'can_hide': forum_acl.get('can_hide_threads'),
+        'can_change_label': forum_acl.get('can_change_threads_labels') == 2,
+        'can_pin': forum_acl.get('can_pin_threads'),
+        'can_close': forum_acl.get('can_close_threads'),
+        'can_move': forum_acl.get('can_move_threads'),
+        'can_review': forum_acl.get('can_review_moderated_content'),
+    })
+
+    if can_change_owned_thread(user, thread):
+        if not thread.acl['can_change_label']:
+            can_change_label = forum_acl.get('can_change_threads_labels') == 1
+            thread.acl['can_change_label'] = can_change_label
+        if not thread.acl['can_hide']:
+            thread.acl['can_hide'] = forum_acl.get('can_hide_own_threads')
 
 
 
 
 def add_acl_to_post(user, post):
 def add_acl_to_post(user, post):
@@ -311,8 +325,7 @@ def allow_see_thread(user, target):
     forum_acl = user.acl['forums'].get(target.forum_id, {})
     forum_acl = user.acl['forums'].get(target.forum_id, {})
     if not forum_acl.get('can_see_all_threads'):
     if not forum_acl.get('can_see_all_threads'):
         if user.is_anonymous() or user.pk != target.starter_id:
         if user.is_anonymous() or user.pk != target.starter_id:
-            message = _("You can't see other users threads in this forum.")
-            raise PermissionDenied(user)
+            raise Http404()
 can_see_thread = return_boolean(allow_see_thread)
 can_see_thread = return_boolean(allow_see_thread)
 
 
 
 
@@ -328,6 +341,40 @@ def allow_start_thread(user, target):
 can_start_thread = return_boolean(allow_start_thread)
 can_start_thread = return_boolean(allow_start_thread)
 
 
 
 
+def allow_reply_thread(user, target):
+    if target.forum.is_closed:
+        message = _("This forum is closed. You can't start new threads in it.")
+        raise PermissionDenied(message)
+    if user.is_anonymous():
+        raise PermissionDenied(_("You have to sign in to start new thread."))
+    if not user.acl['forums'].get(target.id, {'can_start_threads': False}):
+        raise PermissionDenied(_("You don't have permission to start "
+                                 "new threads in this forum."))
+can_start_thread = return_boolean(allow_start_thread)
+
+
+def can_change_owned_thread(user, target):
+    forum_acl = user.acl['forums'].get(target.forum_id, {})
+
+    if user.is_anonymous() or user.pk != target.starter_id:
+        return False
+
+    if target.forum.is_closed or target.is_closed:
+        return False
+
+    if target.first_post.is_protected:
+        return False
+
+    if forum_acl.get('thread_edit_time'):
+        diff = timezone.now() - target.started_on
+        diff_minutes = (diff.days * 24 * 60) + diff.minutes
+
+        if diff_minutes > forum_acl.get('thread_edit_time'):
+            return False
+
+    return True
+
+
 """
 """
 Queryset helpers
 Queryset helpers
 """
 """

+ 15 - 1
misago/threads/signals.py

@@ -4,7 +4,7 @@ from django.dispatch import receiver, Signal
 from misago.core.pgutils import batch_update, batch_delete
 from misago.core.pgutils import batch_update, batch_delete
 from misago.forums.models import Forum
 from misago.forums.models import Forum
 
 
-from misago.threads.models import Thread, Post, Event
+from misago.threads.models import Thread, Post, Event, Label
 
 
 
 
 delete_post = Signal()
 delete_post = Signal()
@@ -29,6 +29,12 @@ def move_thread_content(sender, **kwargs):
     sender.post_set.update(forum=sender.forum)
     sender.post_set.update(forum=sender.forum)
     sender.event_set.update(forum=sender.forum)
     sender.event_set.update(forum=sender.forum)
 
 
+    # remove unavailable labels
+    if sender.label_id:
+        new_forum_labels = Label.objects.get_forum_labels(sender.forum)
+        if sender.label_id not in [l.pk for l in new_forum_labels]:
+            sender.label = None
+
 
 
 from misago.forums.signals import delete_forum_content, move_forum_content
 from misago.forums.signals import delete_forum_content, move_forum_content
 @receiver(delete_forum_content)
 @receiver(delete_forum_content)
@@ -45,6 +51,14 @@ def move_forum_threads(sender, **kwargs):
     Post.objects.filter(forum=sender).update(forum=new_forum)
     Post.objects.filter(forum=sender).update(forum=new_forum)
     Event.objects.filter(forum=sender).update(forum=new_forum)
     Event.objects.filter(forum=sender).update(forum=new_forum)
 
 
+    # move labels
+    old_forum_labels = Label.objects.get_forum_labels(sender)
+    new_forum_labels = Label.objects.get_forum_labels(new_forum)
+
+    for label in old_forum_labels:
+        if label not in new_forum_labels:
+            label.forums.add(new_forum_labels)
+
 
 
 from misago.users.signals import delete_user_content, username_changed
 from misago.users.signals import delete_user_content, username_changed
 @receiver(delete_user_content)
 @receiver(delete_user_content)

+ 268 - 12
misago/threads/tests/test_thread_view.py

@@ -1,33 +1,179 @@
+from django.core.urlresolvers import reverse
+
+from misago.acl import add_acl
+from misago.acl.testutils import override_acl
+from misago.forums.models import Forum
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
-from misago.threads.models import Thread
+from misago.threads.models import Thread, Label
 from misago.threads.testutils import post_thread, reply_thread
 from misago.threads.testutils import post_thread, reply_thread
 
 
 
 
-class ThreadViewTests(AuthenticatedUserTestCase):
+class ThreadViewTestCase(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
-        super(ThreadViewTests, self).setUp()
+        super(ThreadViewTestCase, self).setUp()
 
 
         self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
         self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.forum.labels = []
+
         self.thread = post_thread(self.forum)
         self.thread = post_thread(self.forum)
 
 
-    def override_acl(self, new_acl):
-        new_acl.update({'can_browse': True})
+    def override_acl(self, new_acl, forum=None):
+        forum = forum or self.forum
+
+        new_acl.update({'can_see': True, 'can_browse': True})
 
 
         forums_acl = self.user.acl
         forums_acl = self.user.acl
-        forums_acl['visible_forums'].append(self.forum.pk)
-        forums_acl['forums'][self.forum.pk] = new_acl
+        forums_acl['visible_forums'].append(forum.pk)
+        forums_acl['forums'][forum.pk] = new_acl
         override_acl(self.user, forums_acl)
         override_acl(self.user, forums_acl)
 
 
-        self.forum.acl = {}
-        add_acl(self.user, self.forum)
-
     def reload_thread(self):
     def reload_thread(self):
-        self.thread = Thread.objects.get(id=thread.id)
+        self.thread = Thread.objects.get(id=self.thread.id)
         return self.thread
         return self.thread
 
 
 
 
-class ThreadViewModerationTests(ThreadViewTests):
+class ThreadViewTests(ThreadViewTestCase):
+    def test_can_see_all_threads_false(self):
+        """its impossible to see thread made by other user"""
+        self.override_acl({
+            'can_see_all_threads': False,
+            'can_see_own_threads': True
+        })
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 404)
+
+    def test_can_see_all_threads_false_owned_thread(self):
+        """user can see thread he started in private forum"""
+        self.override_acl({
+            'can_see_all_threads': False,
+            'can_see_own_threads': True
+        })
+
+        self.thread.starter = self.user
+        self.thread.save()
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)
+
+    def test_can_see_all_threads_true(self):
+        """its possible to see thread made by other user"""
+        self.override_acl({
+            'can_see_all_threads': True,
+            'can_see_own_threads': False
+        })
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)
+
+
+class ThreadViewModerationTests(ThreadViewTestCase):
+    def setUp(self):
+        super(ThreadViewModerationTests, self).setUp()
+        Label.objects.clear_cache()
+
+    def tearDown(self):
+        super(ThreadViewModerationTests, self).tearDown()
+        Label.objects.clear_cache()
+
+    def override_acl(self, new_acl, forum=None):
+        new_acl.update({
+            'can_see_all_threads': True,
+            'can_see_own_threads': False
+        })
+        super(ThreadViewModerationTests, self).override_acl(new_acl, forum)
+
+    def test_label_thread(self):
+        """its possible to set thread label"""
+        self.override_acl({'can_change_threads_labels': 0})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn("Moderate thread", response.content)
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn("Moderate thread", response.content)
+
+        test_label = Label.objects.create(name="Foxtrot", slug="foxtrot")
+        test_label.forums.add(self.forum)
+        Label.objects.clear_cache()
+
+        self.override_acl({'can_change_threads_labels': 0})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn("Moderate thread", response.content)
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(test_label.name, response.content)
+        self.assertIn(test_label.slug, response.content)
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'thread_action': 'label:%s' % test_label.slug
+        })
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(self.reload_thread().label_id, test_label.id)
+
+    def test_change_thread_label(self):
+        """its possible to change thread label"""
+        test_label = Label.objects.create(name="Foxtrot", slug="foxtrot")
+        test_label.forums.add(self.forum)
+        other_label = Label.objects.create(name="Uniform", slug="uniform")
+        other_label.forums.add(self.forum)
+
+        Label.objects.clear_cache()
+
+        self.thread.label = test_label
+        self.thread.save()
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn(test_label.name, response.content)
+        self.assertNotIn(test_label.slug, response.content)
+        self.assertIn(other_label.name, response.content)
+        self.assertIn(other_label.slug, response.content)
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'thread_action': 'label:%s' % test_label.slug
+        })
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'thread_action': 'label:%s' % other_label.slug
+        })
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(self.reload_thread().label_id, other_label.id)
+
+    def test_unlabel_thread(self):
+        """its possible to unset thread label"""
+        test_label = Label.objects.create(name="Foxtrot", slug="foxtrot")
+        test_label.forums.add(self.forum)
+        Label.objects.clear_cache()
+
+        self.thread.label = test_label
+        self.thread.save()
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('unlabel', response.content)
+
+        self.override_acl({'can_change_threads_labels': 2})
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'thread_action': 'unlabel'
+        })
+        self.assertEqual(response.status_code, 302)
+        self.assertIsNone(self.reload_thread().label)
+
     def test_pin_thread(self):
     def test_pin_thread(self):
         """its possible to pin thread"""
         """its possible to pin thread"""
         self.override_acl({'can_pin_threads': 0})
         self.override_acl({'can_pin_threads': 0})
@@ -58,3 +204,113 @@ class ThreadViewModerationTests(ThreadViewTests):
                                     data={'thread_action': 'unpin'})
                                     data={'thread_action': 'unpin'})
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertFalse(self.reload_thread().is_pinned)
         self.assertFalse(self.reload_thread().is_pinned)
+
+    def test_close_thread(self):
+        """its possible to close thread"""
+        self.override_acl({'can_close_threads': 0})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'close'})
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_close_threads': 2})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'close'})
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue(self.reload_thread().is_closed)
+
+    def test_open_thread(self):
+        """its possible to close thread"""
+        self.thread.is_closed = True
+        self.thread.save()
+
+        self.override_acl({'can_close_threads': 0})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'open'})
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_close_threads': 2})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'open'})
+        self.assertEqual(response.status_code, 302)
+        self.assertFalse(self.reload_thread().is_closed)
+
+    def test_move_thread(self):
+        """its possible to move thread"""
+        self.override_acl({'can_move_threads': 0})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'move'})
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_move_threads': 1})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'move'})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("Move thread to forum:", response.content)
+
+        new_forum = Forum(name="New Forum",
+                          slug="new-forum",
+                          role="forum")
+        new_forum.insert_at(self.forum.parent, save=True)
+
+        self.override_acl({'can_move_threads': 1})
+        self.override_acl({'can_move_threads': 1}, new_forum)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'thread_action': 'move',
+            'new_forum': unicode(new_forum.id),
+            'submit': '1'
+        })
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(self.reload_thread().forum, new_forum)
+
+        # we made forum "empty", assert that board index renders
+        response = self.client.get(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_hide_thread(self):
+        """its possible to hide thread"""
+        self.override_acl({'can_hide_threads': 0})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'hide'})
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_hide_threads': 2})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'hide'})
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue(self.reload_thread().is_hidden)
+
+        # we made forum "empty", assert that board index renders
+        response = self.client.get(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_unhide_thread(self):
+        """its possible to hide thread"""
+        self.thread.is_hidden = True
+        self.thread.save()
+
+        self.override_acl({'can_hide_threads': 0})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'unhide'})
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_hide_threads': 2})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'unhide'})
+        self.assertEqual(response.status_code, 302)
+        self.assertFalse(self.reload_thread().is_hidden)
+
+    def test_delete_thread(self):
+        """its possible to delete thread"""
+        self.override_acl({'can_hide_threads': 0})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'delete'})
+        self.assertEqual(response.status_code, 200)
+
+        self.override_acl({'can_hide_threads': 2})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'delete'})
+        self.assertEqual(response.status_code, 302)
+
+        # we made forum empty, assert that board index renders
+        response = self.client.get(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)

+ 19 - 13
misago/threads/views/generic/forum/actions.py

@@ -187,10 +187,12 @@ class ForumActions(Actions):
                 request.POST, acl=request.user.acl, forum=self.forum)
                 request.POST, acl=request.user.acl, forum=self.forum)
             if form.is_valid():
             if form.is_valid():
                 new_forum = form.cleaned_data['new_forum']
                 new_forum = form.cleaned_data['new_forum']
-                for thread in threads:
-                    moderation.move_thread(request.user, thread, new_forum)
-
                 with atomic():
                 with atomic():
+                    self.forum.lock()
+
+                    for thread in threads:
+                        moderation.move_thread(request.user, thread, new_forum)
+
                     self.forum.synchronize()
                     self.forum.synchronize()
                     self.forum.save()
                     self.forum.save()
                     new_forum.synchronize()
                     new_forum.synchronize()
@@ -339,15 +341,17 @@ class ForumActions(Actions):
 
 
     def action_hide(self, request, threads):
     def action_hide(self, request, threads):
         changed_threads = 0
         changed_threads = 0
-        for thread in threads:
-            if moderation.hide_thread(request.user, thread):
-                changed_threads += 1
+        with atomic():
+            self.forum.lock()
+            for thread in threads:
+                if moderation.hide_thread(request.user, thread):
+                    changed_threads += 1
 
 
-        if changed_threads:
-            with atomic():
+            if changed_threads:
                 self.forum.synchronize()
                 self.forum.synchronize()
                 self.forum.save()
                 self.forum.save()
 
 
+        if changed_threads:
             message = ungettext(
             message = ungettext(
                 '%(changed)d thread was hidden.',
                 '%(changed)d thread was hidden.',
                 '%(changed)d threads were hidden.',
                 '%(changed)d threads were hidden.',
@@ -359,15 +363,17 @@ class ForumActions(Actions):
 
 
     def action_delete(self, request, threads):
     def action_delete(self, request, threads):
         changed_threads = 0
         changed_threads = 0
-        for thread in threads:
-            if moderation.delete_thread(request.user, thread):
-                changed_threads += 1
+        with atomic():
+            self.forum.lock()
+            for thread in threads:
+                if moderation.delete_thread(request.user, thread):
+                    changed_threads += 1
 
 
-        if changed_threads:
-            with atomic():
+            if changed_threads:
                 self.forum.synchronize()
                 self.forum.synchronize()
                 self.forum.save()
                 self.forum.save()
 
 
+        if changed_threads:
             message = ungettext(
             message = ungettext(
                 '%(changed)d thread was deleted.',
                 '%(changed)d thread was deleted.',
                 '%(changed)d threads were deleted.',
                 '%(changed)d threads were deleted.',

+ 17 - 8
misago/threads/views/generic/thread/threadactions.py

@@ -24,7 +24,7 @@ class ThreadActions(ActionsBase):
 
 
         actions = []
         actions = []
 
 
-        if self.forum.acl['can_change_threads_labels'] == 2:
+        if self.thread.acl['can_change_label']:
             self.forum.labels = Label.objects.get_forum_labels(self.forum)
             self.forum.labels = Label.objects.get_forum_labels(self.forum)
             for label in self.forum.labels:
             for label in self.forum.labels:
                 if label.pk != self.thread.label_id:
                 if label.pk != self.thread.label_id:
@@ -42,7 +42,7 @@ class ThreadActions(ActionsBase):
                     'name': _("Remove label")
                     'name': _("Remove label")
                 })
                 })
 
 
-        if self.forum.acl['can_pin_threads']:
+        if self.thread.acl['can_pin']:
             if self.thread.is_pinned:
             if self.thread.is_pinned:
                 actions.append({
                 actions.append({
                     'action': 'unpin',
                     'action': 'unpin',
@@ -56,7 +56,7 @@ class ThreadActions(ActionsBase):
                     'name': _("Pin thread")
                     'name': _("Pin thread")
                 })
                 })
 
 
-        if self.forum.acl['can_review_moderated_content']:
+        if self.thread.acl['can_review']:
             if self.thread.is_moderated:
             if self.thread.is_moderated:
                 actions.append({
                 actions.append({
                     'action': 'approve',
                     'action': 'approve',
@@ -64,14 +64,14 @@ class ThreadActions(ActionsBase):
                     'name': _("Approve thread")
                     'name': _("Approve thread")
                 })
                 })
 
 
-        if self.forum.acl['can_move_threads']:
+        if self.thread.acl['can_move']:
             actions.append({
             actions.append({
                 'action': 'move',
                 'action': 'move',
                 'icon': 'arrow-right',
                 'icon': 'arrow-right',
                 'name': _("Move thread")
                 'name': _("Move thread")
             })
             })
 
 
-        if self.forum.acl['can_close_threads']:
+        if self.thread.acl['can_close']:
             if self.thread.is_closed:
             if self.thread.is_closed:
                 actions.append({
                 actions.append({
                     'action': 'open',
                     'action': 'open',
@@ -85,7 +85,7 @@ class ThreadActions(ActionsBase):
                     'name': _("Close thread")
                     'name': _("Close thread")
                 })
                 })
 
 
-        if self.forum.acl['can_hide_threads']:
+        if self.thread.acl['can_hide']:
             if self.thread.is_hidden:
             if self.thread.is_hidden:
                 actions.append({
                 actions.append({
                     'action': 'unhide',
                     'action': 'unhide',
@@ -99,7 +99,7 @@ class ThreadActions(ActionsBase):
                     'name': _("Hide thread")
                     'name': _("Hide thread")
                 })
                 })
 
 
-        if self.forum.acl['can_hide_threads'] == 2:
+        if self.thread.acl['can_hide'] == 2:
             actions.append({
             actions.append({
                 'action': 'delete',
                 'action': 'delete',
                 'icon': 'times',
                 'icon': 'times',
@@ -146,6 +146,7 @@ class ThreadActions(ActionsBase):
                 new_forum = form.cleaned_data['new_forum']
                 new_forum = form.cleaned_data['new_forum']
 
 
                 with atomic():
                 with atomic():
+                    self.forum.lock()
                     moderation.move_thread(request.user, thread, new_forum)
                     moderation.move_thread(request.user, thread, new_forum)
                     self.forum.synchronize()
                     self.forum.synchronize()
                     self.forum.save()
                     self.forum.save()
@@ -181,14 +182,22 @@ class ThreadActions(ActionsBase):
 
 
     def action_unhide(self, request, thread):
     def action_unhide(self, request, thread):
         moderation.unhide_thread(request.user, thread)
         moderation.unhide_thread(request.user, thread)
+        self.forum.synchronize()
+        self.forum.save()
         messages.success(request, _("Thread was made visible."))
         messages.success(request, _("Thread was made visible."))
 
 
     def action_hide(self, request, thread):
     def action_hide(self, request, thread):
-        moderation.hide_thread(request.user, thread)
+        with atomic():
+            self.forum.lock()
+            moderation.hide_thread(request.user, thread)
+            self.forum.synchronize()
+            self.forum.save()
+
         messages.success(request, _("Thread was hid."))
         messages.success(request, _("Thread was hid."))
 
 
     def action_delete(self, request, thread):
     def action_delete(self, request, thread):
         with atomic():
         with atomic():
+            self.forum.lock()
             moderation.delete_thread(request.user, thread)
             moderation.delete_thread(request.user, thread)
             self.forum.synchronize()
             self.forum.synchronize()
             self.forum.save()
             self.forum.save()