Browse Source

Delete/hide/undelete checkpoints #62

Ralfp 12 years ago
parent
commit
bb2f9b850c

+ 14 - 0
misago/acl/permissions/privatethreads.py

@@ -14,6 +14,12 @@ def make_form(request, role, form):
         form.base_fields['private_thread_attachments_limit'] = forms.IntegerField(min_value=0, initial=3, required=False)
         form.base_fields['private_thread_attachments_limit'] = forms.IntegerField(min_value=0, initial=3, required=False)
         form.base_fields['can_invite_ignoring'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['can_invite_ignoring'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['private_threads_mod'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['private_threads_mod'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
+                                                                                     (0, _("No")),
+                                                                                     (1, _("Yes, soft-delete")),
+                                                                                     (2, _("Yes, hard-delete")),
+                                                                                     ), coerce=int)
+
         form.layout.append((
         form.layout.append((
                             _("Private Threads"),
                             _("Private Threads"),
                             (
                             (
@@ -24,6 +30,7 @@ def make_form(request, role, form):
                              ('private_thread_attachments_limit', {'label': _("Max. number of attachments per post")}),
                              ('private_thread_attachments_limit', {'label': _("Max. number of attachments per post")}),
                              ('can_invite_ignoring', {'label': _("Can invite users that ignore him")}),
                              ('can_invite_ignoring', {'label': _("Can invite users that ignore him")}),
                              ('private_threads_mod', {'label': _("Can moderate threads"), 'help_text': _("Makes user with this role Private Threads moderator capable of closing, deleting and editing all private threads he participates in at will.")}),
                              ('private_threads_mod', {'label': _("Can moderate threads"), 'help_text': _("Makes user with this role Private Threads moderator capable of closing, deleting and editing all private threads he participates in at will.")}),
+                             ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
                              ),
                              ),
                             ))
                             ))
 
 
@@ -52,6 +59,8 @@ def build(acl, roles):
     acl.private_threads.acl['private_thread_attachments_limit'] = False
     acl.private_threads.acl['private_thread_attachments_limit'] = False
     acl.private_threads.acl['can_invite_ignoring'] = False
     acl.private_threads.acl['can_invite_ignoring'] = False
     acl.private_threads.acl['private_threads_mod'] = False
     acl.private_threads.acl['private_threads_mod'] = False
+    acl.private_threads.acl['can_delete_checkpoints'] = 0
+    acl.private_threads.acl['can_see_deleted_checkpoints'] = False
 
 
     for role in roles:
     for role in roles:
         for perm, value in acl.private_threads.acl.items():
         for perm, value in acl.private_threads.acl.items():
@@ -94,6 +103,8 @@ def cleanup(acl, perms, forums):
                               'can_delete_polls': 0,
                               'can_delete_polls': 0,
                               'can_delete_attachments': False,
                               'can_delete_attachments': False,
                               'can_invite_ignoring': False,
                               'can_invite_ignoring': False,
+                              'can_delete_checkpoints': 0,
+                              'can_see_deleted_checkpoints': False,
                              }
                              }
 
 
     for perm in perms:
     for perm in perms:
@@ -120,5 +131,8 @@ def cleanup(acl, perms, forums):
                 acl.threads.acl[forum]['can_delete_threads'] = 2
                 acl.threads.acl[forum]['can_delete_threads'] = 2
                 acl.threads.acl[forum]['can_delete_posts'] = 2
                 acl.threads.acl[forum]['can_delete_posts'] = 2
                 acl.threads.acl[forum]['can_delete_attachments'] = True
                 acl.threads.acl[forum]['can_delete_attachments'] = True
+                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
+            if perm['can_delete_checkpoints'] > acl.threads.acl[forum]['can_delete_checkpoints']:
+                acl.threads.acl[forum]['can_delete_checkpoints'] = perm['can_delete_checkpoints']
         except KeyError:
         except KeyError:
             pass
             pass

+ 74 - 16
misago/acl/permissions/threads.py

@@ -46,30 +46,36 @@ def make_forum_form(request, role, form):
     form.base_fields['can_edit_labels'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_edit_labels'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_see_changelog'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_see_changelog'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_pin_threads'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_pin_threads'] = forms.TypedChoiceField(choices=(
-                                                                 (0, _("No")),
-                                                                 (1, _("Yes, to stickies")),
-                                                                 (2, _("Yes, to announcements")),
-                                                                 ), coerce=int)
+                                                                          (0, _("No")),
+                                                                          (1, _("Yes, to stickies")),
+                                                                          (2, _("Yes, to announcements")),
+                                                                          ), coerce=int)
     form.base_fields['can_edit_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_edit_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_move_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_move_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_close_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_close_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_protect_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_protect_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_delete_threads'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_delete_threads'] = forms.TypedChoiceField(choices=(
-                                                                    (0, _("No")),
-                                                                    (1, _("Yes, soft-delete")),
-                                                                    (2, _("Yes, hard-delete")),
-                                                                    ), coerce=int)
+                                                                              (0, _("No")),
+                                                                              (1, _("Yes, soft-delete")),
+                                                                              (2, _("Yes, hard-delete")),
+                                                                              ), coerce=int)
     form.base_fields['can_delete_posts'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_delete_posts'] = forms.TypedChoiceField(choices=(
-                                                                  (0, _("No")),
-                                                                  (1, _("Yes, soft-delete")),
-                                                                  (2, _("Yes, hard-delete")),
-                                                                   ), coerce=int)
+                                                                           (0, _("No")),
+                                                                           (1, _("Yes, soft-delete")),
+                                                                           (2, _("Yes, hard-delete")),
+                                                                           ), coerce=int)
     form.base_fields['can_delete_polls'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_delete_polls'] = forms.TypedChoiceField(choices=(
-                                                                  (0, _("No")),
-                                                                  (1, _("Yes, soft-delete")),
-                                                                  (2, _("Yes, hard-delete")),
-                                                                  ), coerce=int)
+                                                                           (0, _("No")),
+                                                                           (1, _("Yes, soft-delete")),
+                                                                           (2, _("Yes, hard-delete")),
+                                                                           ), coerce=int)
     form.base_fields['can_delete_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_delete_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
+                                                                                 (0, _("No")),
+                                                                                 (1, _("Yes, soft-delete")),
+                                                                                 (2, _("Yes, hard-delete")),
+                                                                                 ), coerce=int)
+    form.base_fields['can_see_deleted_checkpoints'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
 
 
     form.layout.append((
     form.layout.append((
                         _("Threads"),
                         _("Threads"),
@@ -130,6 +136,8 @@ def make_forum_form(request, role, form):
                          ('can_delete_posts', {'label': _("Can delete posts")}),
                          ('can_delete_posts', {'label': _("Can delete posts")}),
                          ('can_delete_polls', {'label': _("Can delete polls")}),
                          ('can_delete_polls', {'label': _("Can delete polls")}),
                          ('can_delete_attachments', {'label': _("Can delete attachments")}),
                          ('can_delete_attachments', {'label': _("Can delete attachments")}),
+                         ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
+                         ('can_see_deleted_checkpoints', {'label': _("Can see deleted checkpoints")}),
                         ),
                         ),
                        ),)
                        ),)
 
 
@@ -546,6 +554,53 @@ class ThreadsACL(BaseACL):
         except KeyError:
         except KeyError:
             raise ACLError403(_("You don't have permission to see who voted on this post."))
             raise ACLError403(_("You don't have permission to see who voted on this post."))
 
 
+    def can_see_checkpoint(self, forum, checkpoint):
+        if not checkpoint.deleted:
+            return True
+        try:
+            return self.acl[forum.pk]['can_see_deleted_checkpoints']
+        except KeyError:
+            raise False
+
+    def can_delete_checkpoint(self, forum):
+        try:
+            return self.acl[forum.pk]['can_delete_checkpoints']
+        except KeyError:
+            raise False
+
+    def allow_checkpoint_view(self, forum, checkpoint):
+        if checkpoint.deleted:
+            try:
+                forum_role = self.acl[forum.pk]
+                if not forum_role['can_see_deleted_checkpoints']:
+                    raise ACLError403(_("Selected checkpoint could not be found."))
+            except KeyError:
+                raise ACLError403(_("Selected checkpoint could not be found."))
+
+    def allow_checkpoint_hide(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_checkpoints']:
+                raise ACLError403(_("You cannot hide checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot hide checkpoints!"))
+
+    def allow_checkpoint_delete(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if forum_role['can_delete_checkpoints'] != 2:
+                raise ACLError403(_("You cannot delete checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot delete checkpoints!"))
+
+    def allow_checkpoint_show(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_checkpoints']:
+                raise ACLError403(_("You cannot show checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot show checkpoints!"))
+
 
 
 def build_forums(acl, perms, forums, forum_roles):
 def build_forums(acl, perms, forums, forum_roles):
     acl.threads = ThreadsACL()
     acl.threads = ThreadsACL()
@@ -582,7 +637,10 @@ def build_forums(acl, perms, forums, forum_roles):
                      'can_delete_posts': 0,
                      'can_delete_posts': 0,
                      'can_delete_polls': 0,
                      'can_delete_polls': 0,
                      'can_delete_attachments': False,
                      'can_delete_attachments': False,
+                     'can_see_deleted_checkpoints': False,
+                     'can_delete_checkpoints': 0,
                      }
                      }
+
         for perm in perms:
         for perm in perms:
             try:
             try:
                 role = forum_roles[perm['forums'][forum.pk]]
                 role = forum_roles[perm['forums'][forum.pk]]

+ 12 - 0
misago/apps/privatethreads/delete.py

@@ -14,4 +14,16 @@ class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
 
 
 
 
 class HideReplyView(HideReplyBaseView, TypeMixin):
 class HideReplyView(HideReplyBaseView, TypeMixin):
+    pass
+
+
+class DeleteCheckpointView(DeleteCheckpointBaseView, TypeMixin):
+    pass
+
+
+class HideCheckpointView(HideCheckpointBaseView, TypeMixin):
+    pass
+
+
+class ShowCheckpointView(ShowCheckpointBaseView, TypeMixin):
     pass
     pass

+ 7 - 4
misago/apps/privatethreads/urls.py

@@ -21,10 +21,13 @@ urlpatterns = patterns('misago.apps.privatethreads',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="private_thread_unwatch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="private_thread_unwatch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/invite/$', 'jumps.InviteUserView', name="private_thread_invite_user"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/invite/$', 'jumps.InviteUserView', name="private_thread_invite_user"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/remove/$', 'jumps.RemoveUserView', name="private_thread_remove_user"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/remove/$', 'jumps.RemoveUserView', name="private_thread_remove_user"),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="private_thread_delete", kwargs={'mode': 'delete_thread'}),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="private_thread_hide", kwargs={'mode': 'hide_thread'}),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="private_post_delete", kwargs={'mode': 'delete_post'}),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="private_post_hide", kwargs={'mode': 'hide_post'}),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="private_thread_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="private_thread_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="private_post_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="private_post_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/delete/$', 'delete.DeleteCheckpointView', name="private_post_checkpoint_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/hide/$', 'delete.HideCheckpointView', name="private_post_checkpoint_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/show/$', 'delete.ShowCheckpointView', name="private_post_checkpoint_show"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="private_post_info"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="private_post_info"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="private_thread_changelog"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="private_thread_changelog"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),

+ 12 - 0
misago/apps/threads/delete.py

@@ -14,4 +14,16 @@ class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
 
 
 
 
 class HideReplyView(HideReplyBaseView, TypeMixin):
 class HideReplyView(HideReplyBaseView, TypeMixin):
+    pass
+
+
+class DeleteCheckpointView(DeleteCheckpointBaseView, TypeMixin):
+    pass
+
+
+class HideCheckpointView(HideCheckpointBaseView, TypeMixin):
+    pass
+
+
+class ShowCheckpointView(ShowCheckpointBaseView, TypeMixin):
     pass
     pass

+ 7 - 4
misago/apps/threads/urls.py

@@ -22,10 +22,13 @@ urlpatterns = patterns('misago.apps.threads',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="thread_unwatch_email"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="thread_unwatch_email"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', 'jumps.UpvotePostView', name="post_upvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', 'jumps.UpvotePostView', name="post_upvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', 'jumps.DownvotePostView', name="post_downvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', 'jumps.DownvotePostView', name="post_downvote"),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="thread_delete", kwargs={'mode': 'delete_thread'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="thread_hide", kwargs={'mode': 'hide_thread'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="post_delete", kwargs={'mode': 'delete_post'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="post_hide", kwargs={'mode': 'hide_post'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="thread_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="thread_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="post_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="post_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/delete/$', 'delete.DeleteCheckpointView', name="post_checkpoint_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/hide/$', 'delete.HideCheckpointView', name="post_checkpoint_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/show/$', 'delete.ShowCheckpointView', name="post_checkpoint_show"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="post_info"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="post_info"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', 'details.KarmaVotesView', name="post_votes"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', 'details.KarmaVotesView', name="post_votes"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="thread_changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="thread_changelog"),

+ 57 - 2
misago/apps/threadtype/delete.py

@@ -5,10 +5,13 @@ from django.utils.translation import ugettext as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
 from misago.messages import Message
 from misago.messages import Message
-from misago.models import Forum, Thread, Post
+from misago.models import Forum, Thread, Post, Checkpoint
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
 
 
 class DeleteHideBaseView(ViewBase):
 class DeleteHideBaseView(ViewBase):
+    def set_context(self):
+        pass
+
     def _set_context(self):
     def _set_context(self):
         self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
         self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
         self.forum = self.thread.forum
         self.forum = self.thread.forum
@@ -23,6 +26,10 @@ class DeleteHideBaseView(ViewBase):
             self.post = self.thread.post_set.get(id=self.kwargs.get('post'))
             self.post = self.thread.post_set.get(id=self.kwargs.get('post'))
             self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
             self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
 
 
+        if self.kwargs.get('checkpoint'):
+            self.checkpoint = self.post.checkpoint_set.get(id=self.kwargs.get('checkpoint'))
+            self.request.acl.threads.allow_checkpoint_view(self.forum, self.checkpoint)
+
         self.set_context()
         self.set_context()
 
 
     def __call__(self, request, **kwargs):
     def __call__(self, request, **kwargs):
@@ -36,7 +43,7 @@ class DeleteHideBaseView(ViewBase):
             self.delete()
             self.delete()
             self.message()
             self.message()
             return self.response()
             return self.response()
-        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Checkpoint.DoesNotExist):
             return error404(request)
             return error404(request)
         except ACLError403 as e:
         except ACLError403 as e:
             return error403(request, unicode(e))
             return error403(request, unicode(e))
@@ -142,3 +149,51 @@ class HideReplyBaseView(DeleteHideBaseView):
 
 
     def response(self):
     def response(self):
         return self.redirect_to_post(self.post)
         return self.redirect_to_post(self.post)
+
+
+class DeleteCheckpointBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_delete(self.forum)
+
+    def delete(self):
+        self.checkpoint.delete()
+        self.post.checkpoints = self.post.checkpoint_set.count() > 0
+        self.post.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected checkpoint has been deleted.")), 'success', 'threads_%s' % self.post.pk)
+
+    def response(self):
+        return self.redirect_to_post(self.post)
+
+
+class HideCheckpointBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_hide(self.forum)
+        if self.checkpoint.deleted:
+            raise ACLError403(_('This checkpoint is already hidden!'))
+
+    def delete(self):
+        self.checkpoint.deleted = True
+        self.checkpoint.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected checkpoint has been hidden.")), 'success', 'threads_%s' % self.post.pk)
+
+    def response(self):
+        return self.redirect_to_post(self.post)
+
+
+class ShowCheckpointBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_show(self.forum)
+
+    def delete(self):
+        self.checkpoint.deleted = False
+        self.checkpoint.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected checkpoint has been made visible.")), 'success', 'threads_%s' % self.post.pk)
+
+    def response(self):
+        return self.redirect_to_post(self.post)

+ 5 - 0
misago/apps/threadtype/thread/views.py

@@ -61,6 +61,11 @@ class ThreadBaseView(ViewBase):
             post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
             post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
             if post.ignored:
             if post.ignored:
                 self.ignored = True
                 self.ignored = True
+            post.checkpoints = []
+            if post.checkpoint_set.all():
+                for checkpoint in post.checkpoint_set.all():
+                    if self.request.acl.threads.can_see_checkpoint(self.forum, checkpoint):
+                        post.checkpoints.append(checkpoint)
 
 
         last_post = self.posts[len(self.posts) - 1]
         last_post = self.posts[len(self.posts) - 1]
 
 

+ 2 - 0
misago/fixtures/forumsroles.py

@@ -38,6 +38,8 @@ def load():
                         'can_delete_posts': 2,
                         'can_delete_posts': 2,
                         'can_delete_polls': 2,
                         'can_delete_polls': 2,
                         'can_delete_attachments': True,
                         'can_delete_attachments': True,
+                        'can_see_deleted_checkpoints': True,
+                        'can_delete_checkpoints': 2,
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)
 
 

+ 2 - 0
misago/fixtures/userroles.py

@@ -22,6 +22,7 @@ def load():
                         'private_thread_attachments_limit': 0,
                         'private_thread_attachments_limit': 0,
                         'can_invite_ignoring': True,
                         'can_invite_ignoring': True,
                         'private_threads_mod': True,
                         'private_threads_mod': True,
+                        'can_delete_checkpoints': 2,
                         'forums': {3: 1, 5: 1, 6: 1},
                         'forums': {3: 1, 5: 1, 6: 1},
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)
@@ -44,6 +45,7 @@ def load():
                         'private_thread_attachments_limit': 0,
                         'private_thread_attachments_limit': 0,
                         'can_invite_ignoring': True,
                         'can_invite_ignoring': True,
                         'private_threads_mod': True,
                         'private_threads_mod': True,
+                        'can_delete_checkpoints': 1,
                         'forums': {3: 1, 5: 1, 6: 1},
                         'forums': {3: 1, 5: 1, 6: 1},
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)

+ 400 - 0
misago/migrations/0004_auto__add_field_checkpoint_deleted.py

@@ -0,0 +1,400 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Checkpoint.deleted'
+        db.add_column(u'misago_checkpoint', 'deleted',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Checkpoint.deleted'
+        db.delete_column(u'misago_checkpoint', 'deleted')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.themeadjustment': {
+            'Meta': {'object_name': 'ThemeAdjustment'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'useragents': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 1 - 0
misago/models/checkpointmodel.py

@@ -19,6 +19,7 @@ class Checkpoint(models.Model):
     date = models.DateTimeField()
     date = models.DateTimeField()
     ip = models.GenericIPAddressField()
     ip = models.GenericIPAddressField()
     agent = models.CharField(max_length=255)
     agent = models.CharField(max_length=255)
+    deleted = models.BooleanField(default=False)
 
 
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'

+ 5 - 1
static/cranefly/css/cranefly.css

@@ -1132,9 +1132,13 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .thread-body .post-wrapper .post-body.post-muted .user-avatar{width:50px;height:50px;opacity:0.75;filter:alpha(opacity=75);}
 .thread-body .post-wrapper .post-body.post-muted .user-avatar{width:50px;height:50px;opacity:0.75;filter:alpha(opacity=75);}
 .thread-body .post-wrapper .post-body.post-muted .post-content{margin-left:71px;min-height:0px;opacity:0.75;filter:alpha(opacity=75);padding:14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header{float:right;margin:0px;margin-top:-7px;margin-right:-14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header .post-header-compact{float:left;margin-right:14px;}
 .thread-body .post-wrapper .post-body.post-muted .post-content{margin-left:71px;min-height:0px;opacity:0.75;filter:alpha(opacity=75);padding:14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header{float:right;margin:0px;margin-top:-7px;margin-right:-14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header .post-header-compact{float:left;margin-right:14px;}
 .thread-body .post-wrapper .post-body.post-muted .post-content .post-message{color:#999999;font-size:17.5px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-message strong,.thread-body .post-wrapper .post-body.post-muted .post-content .post-message a{color:#333333;font-weight:normal;}
 .thread-body .post-wrapper .post-body.post-muted .post-content .post-message{color:#999999;font-size:17.5px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-message strong,.thread-body .post-wrapper .post-body.post-muted .post-content .post-message a{color:#333333;font-weight:normal;}
-.thread-body .post-checkpoints .post-checkpoint{text-align:center;margin-bottom:20px;}.thread-body .post-checkpoints .post-checkpoint 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;height:4px;margin-bottom:-12px;}
+.thread-body .post-checkpoints .post-checkpoint{text-align:center;margin-bottom:20px;}.thread-body .post-checkpoints .post-checkpoint.checkpoint-deleted{opacity:0.3;filter:alpha(opacity=30);}.thread-body .post-checkpoints .post-checkpoint.checkpoint-deleted:hover{opacity:0.6;filter:alpha(opacity=60);}
+.thread-body .post-checkpoints .post-checkpoint 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;height:4px;margin-bottom:-12px;}
 .thread-body .post-checkpoints .post-checkpoint span{background-color:#fbfbfb;padding:0px 14px;color:#999999;}.thread-body .post-checkpoints .post-checkpoint span a{color:#333333;}
 .thread-body .post-checkpoints .post-checkpoint span{background-color:#fbfbfb;padding:0px 14px;color:#999999;}.thread-body .post-checkpoints .post-checkpoint span a{color:#333333;}
 .thread-body .post-checkpoints .post-checkpoint span i{opacity:0.43;filter:alpha(opacity=43);}
 .thread-body .post-checkpoints .post-checkpoint span i{opacity:0.43;filter:alpha(opacity=43);}
+.thread-body .post-checkpoints .post-checkpoint span form{display:inline-block;margin:0px;margin-top:-3px;margin-left:7px;padding:0px;}.thread-body .post-checkpoints .post-checkpoint span form .btn{margin-top:-2px;padding:0px;font-weight:normal;}.thread-body .post-checkpoints .post-checkpoint span form .btn:active,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover{text-decoration:underline;}.thread-body .post-checkpoints .post-checkpoint span form .btn:active.btn-show,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover.btn-show,.thread-body .post-checkpoints .post-checkpoint span form .btn:active.btn-hide,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover.btn-hide{color:#f89406;}
+.thread-body .post-checkpoints .post-checkpoint span form .btn:active.btn-delete,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover.btn-delete{color:#cf402e;}
+.thread-body .post-checkpoints .post-checkpoint span form:first-of-type{margin-left:14px;}
 .thread-moderation{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:3px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;overflow:auto;padding:7px;}.thread-moderation form{margin:0px;}
 .thread-moderation{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:3px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;overflow:auto;padding:7px;}.thread-moderation form{margin:0px;}
 .thread-quick-reply{overflow:auto;margin-top:20px;}.thread-quick-reply .user-avatar{border-radius:3px;float:left;width:100px;height:100px;overflow:visible;}
 .thread-quick-reply{overflow:auto;margin-top:20px;}.thread-quick-reply .user-avatar{border-radius:3px;float:left;width:100px;height:100px;overflow:visible;}
 .thread-quick-reply .editor{margin-left:121px;position:relative;}.thread-quick-reply .editor:after,.thread-quick-reply .editor:before{right:100%;border:solid transparent;content:"";height:0;width:0;position:absolute;pointer-events:none;}
 .thread-quick-reply .editor{margin-left:121px;position:relative;}.thread-quick-reply .editor:after,.thread-quick-reply .editor:before{right:100%;border:solid transparent;content:"";height:0;width:0;position:absolute;pointer-events:none;}

+ 39 - 0
static/cranefly/css/cranefly/thread.less

@@ -356,6 +356,14 @@
 
 
       margin-bottom: @baseLineHeight;
       margin-bottom: @baseLineHeight;
 
 
+      &.checkpoint-deleted {
+        .opacity(30);
+
+        &:hover {
+          .opacity(60);
+        }
+      }
+
       hr {
       hr {
         background-color: @grayLight;
         background-color: @grayLight;
         background-image: -webkit-gradient(linear, 0 0, 100% 100%,
         background-image: -webkit-gradient(linear, 0 0, 100% 100%,
@@ -399,6 +407,37 @@
         i {
         i {
           .opacity(43);
           .opacity(43);
         }
         }
+
+        form {
+          display: inline-block;
+          margin: 0px;
+          margin-top: -3px;
+          margin-left: @baseFontSize / 2;
+          padding: 0px;
+
+          .btn {
+            margin-top: -2px;
+            padding: 0px;
+
+            font-weight: normal;
+
+            &:active, &:hover {
+              text-decoration: underline;
+
+              &.btn-show, &.btn-hide {
+                color: @orange;
+              }
+
+              &.btn-delete {
+                color: @red;
+              }
+            }
+          }
+        }
+
+        form:first-of-type {
+          margin-left: @baseFontSize;
+        }
       }
       }
     }
     }
   }
   }

+ 25 - 0
templates/cranefly/private_threads/thread.html

@@ -329,6 +329,27 @@
               {%- elif checkpoint.action == 'left' -%}
               {%- elif checkpoint.action == 'left' -%}
               <i class="icon-remove-sign"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} left thread {{ date }}{% endtrans %}
               <i class="icon-remove-sign"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} left thread {{ date }}{% endtrans %}
               {%- endif -%}
               {%- endif -%}
+              {% if user.is_authenticated() %}
+              {% if acl.threads.can_delete_checkpoint(forum) %}
+              {% if checkpoint.deleted %}
+              <form action="{% url 'private_post_checkpoint_show' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-show">{% trans %}Show{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'private_post_checkpoint_hide' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
+              {% if acl.threads.can_delete_checkpoint(forum) == 2 %}
+              <form action="{% url 'private_post_checkpoint_delete' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline prompt-delete-checkpoint">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-delete">{% trans %}Delete{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
             </span>
             </span>
           </div>
           </div>
           {% endfor %}
           {% endfor %}
@@ -479,6 +500,10 @@
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           return decision;
           return decision;
       });
       });
+      $('.prompt-delete-checkpoint').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this checkpoint?{% endtrans %}");
+          return decision;
+      });
     });
     });
   </script>
   </script>
   {% if acl.threads.can_reply(forum, thread) %}
   {% if acl.threads.can_reply(forum, thread) %}

+ 28 - 3
templates/cranefly/threads/thread.html

@@ -338,10 +338,10 @@
       {% endif %}
       {% endif %}
     </div>
     </div>
 
 
-    {% if post.checkpoint_set.all() %}
+    {% if post.checkpoints %}
     <div class="post-checkpoints">
     <div class="post-checkpoints">
-      {% for checkpoint in post.checkpoint_set.all() %}
-      <div class="post-checkpoint">
+      {% for checkpoint in post.checkpoints %}
+      <div class="post-checkpoint{% if checkpoint.deleted %} checkpoint-deleted{% endif %}">
         <hr>
         <hr>
         <span>
         <span>
           {%- if checkpoint.action == 'limit' -%}
           {%- if checkpoint.action == 'limit' -%}
@@ -359,6 +359,27 @@
           {%- elif checkpoint.action == 'moved' and acl.forums.can_see(checkpoint.old_forum_id) -%}
           {%- elif checkpoint.action == 'moved' and acl.forums.can_see(checkpoint.old_forum_id) -%}
           <i class="icon-arrow-right"></i> {% trans user=checkpoint_user(checkpoint), forum=checkpoint_forum(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} moved this thread from {{ forum }} {{ date }}{% endtrans %}
           <i class="icon-arrow-right"></i> {% trans user=checkpoint_user(checkpoint), forum=checkpoint_forum(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} moved this thread from {{ forum }} {{ date }}{% endtrans %}
           {%- endif -%}
           {%- endif -%}
+          {% if user.is_authenticated() %}
+          {% if acl.threads.can_delete_checkpoint(forum) %}
+          {% if checkpoint.deleted %}
+          <form action="{% url 'post_checkpoint_show' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <button type="submit" class="btn btn-link btn-show">{% trans %}Show{% endtrans %}</button>
+          </form>
+          {% else %}
+          <form action="{% url 'post_checkpoint_hide' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <button type="submit" class="btn btn-link btn-hide">{% trans %}Hide{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
+          {% if acl.threads.can_delete_checkpoint(forum) == 2 %}
+          <form action="{% url 'post_checkpoint_delete' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline prompt-delete-checkpoint">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <button type="submit" class="btn btn-link btn-delete">{% trans %}Delete{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
         </span>
         </span>
       </div>
       </div>
       {% endfor %}
       {% endfor %}
@@ -457,6 +478,10 @@
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           return decision;
           return decision;
       });
       });
+      $('.prompt-delete-checkpoint').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this checkpoint?{% endtrans %}");
+          return decision;
+      });
     });
     });
     {% endif %}
     {% endif %}
   </script>
   </script>