From 520b1e83f461103faa13d35516435acf42be4ab8 Mon Sep 17 00:00:00 2001 From: "Guido A.J. Stevens" Date: Thu, 19 Sep 2013 08:39:52 +0000 Subject: [PATCH] provide "delete own comments" as a configurable option --- plone/app/discussion/browser/comments.pt | 5 +- plone/app/discussion/browser/comments.py | 18 ++- plone/app/discussion/browser/configure.zcml | 6 +- plone/app/discussion/browser/controlpanel.py | 2 + plone/app/discussion/browser/moderation.py | 34 ++++- plone/app/discussion/configure.zcml | 10 ++ plone/app/discussion/interfaces.py | 11 ++ .../discussion/profiles/default/metadata.xml | 2 +- .../tests/functional_test_comments.txt | 140 ++++++++++++++++++ 9 files changed, 217 insertions(+), 11 deletions(-) diff --git a/plone/app/discussion/browser/comments.pt b/plone/app/discussion/browser/comments.pt index 203e813..0348f33 100644 --- a/plone/app/discussion/browser/comments.pt +++ b/plone/app/discussion/browser/comments.pt @@ -36,7 +36,8 @@ has_author_link python:author_home_url and not isAnon; portrait_url python:view.get_commenter_portrait(reply.author_username); review_state python:wtool.getInfoFor(reply, 'review_state', 'none'); - canEdit python:view.can_edit(reply)" + canEdit python:view.can_edit(reply); + canDelete python:view.can_delete(reply)" tal:attributes="class python:'comment replyTreeLevel'+str(depth)+' state-'+str(review_state); id string:${reply/getId}" tal:condition="python:canReview or review_state == 'published'"> @@ -89,7 +90,7 @@ action="" method="post" class="commentactionsform" - tal:condition="python:canReview" + tal:condition="python:canDelete" tal:attributes="action string:${reply/absolute_url}/@@moderate-delete-comment"> - + diff --git a/plone/app/discussion/browser/controlpanel.py b/plone/app/discussion/browser/controlpanel.py index 822ca2b..84ded37 100644 --- a/plone/app/discussion/browser/controlpanel.py +++ b/plone/app/discussion/browser/controlpanel.py @@ -54,6 +54,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): SingleCheckBoxFieldWidget self.fields['edit_comment_enabled'].widgetFactory = \ SingleCheckBoxFieldWidget + self.fields['delete_own_comment_enabled'].widgetFactory = \ + SingleCheckBoxFieldWidget self.fields['anonymous_comments'].widgetFactory = \ SingleCheckBoxFieldWidget self.fields['show_commenter_image'].widgetFactory = \ diff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py index 55110e8..ea92515 100644 --- a/plone/app/discussion/browser/moderation.py +++ b/plone/app/discussion/browser/moderation.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from Acquisition import aq_inner, aq_parent +from AccessControl import getSecurityManager +from zope.component import queryUtility from Products.Five.browser import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile @@ -8,6 +10,8 @@ from Products.CMFCore.utils import getToolByName from Products.statusmessages.interfaces import IStatusMessage +from plone.registry.interfaces import IRegistry +from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import _ from plone.app.discussion.interfaces import IComment @@ -94,17 +98,37 @@ class DeleteComment(BrowserView): comment = aq_inner(self.context) conversation = aq_parent(comment) content_object = aq_parent(conversation) - del conversation[comment.id] - content_object.reindexObject(idxs=['total_comments']) - IStatusMessage(self.context.REQUEST).addStatusMessage( - _("Comment deleted."), - type="info") + # conditional security + # base ZCML condition zope2.deleteObject allows 'delete own object' + # modify this for 'delete_own_comment_allowed' controlpanel setting + if self.can_delete(comment): + del conversation[comment.id] + content_object.reindexObject() + IStatusMessage(self.context.REQUEST).addStatusMessage( + _("Comment deleted."), + type="info") came_from = self.context.REQUEST.HTTP_REFERER # if the referrer already has a came_from in it, don't redirect back if len(came_from) == 0 or 'came_from=' in came_from: came_from = content_object.absolute_url() return self.context.REQUEST.RESPONSE.redirect(came_from) + def can_delete(self, reply): + """By default requires 'Review comments'. + If 'delete own comments' is enabled, requires 'Edit comments'. + """ + if self.is_delete_own_comment_allowed(): + permission = 'Edit comments' + else: + permission = 'Review comments' + return getSecurityManager().checkPermission(permission, + aq_inner(reply)) + + def is_delete_own_comment_allowed(self): + registry = queryUtility(IRegistry) + settings = registry.forInterface(IDiscussionSettings, check=False) + return settings.delete_own_comment_enabled + class PublishComment(BrowserView): """Publish a comment. diff --git a/plone/app/discussion/configure.zcml b/plone/app/discussion/configure.zcml index 0a689b1..d1d2675 100644 --- a/plone/app/discussion/configure.zcml +++ b/plone/app/discussion/configure.zcml @@ -54,6 +54,16 @@ profile="plone.app.discussion:default" /> + + diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index a807580..5a6cc8d 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -254,6 +254,17 @@ class IDiscussionSettings(Interface): default=False, ) + delete_own_comment_enabled = schema.Bool( + title=_(u"label_delete_own_comment_enabled", + default="Allow users to delete their own comment threads"), + description=_(u"help_edit_comment_enabled", + default=u"If selected, users may delete their own " + "comments -> AND the whole reply thread below that " + "comment!"), + required=False, + default=False, + ) + text_transform = schema.Choice( title=_(u"label_text_transform", default="Comment text transform"), diff --git a/plone/app/discussion/profiles/default/metadata.xml b/plone/app/discussion/profiles/default/metadata.xml index 7a20473..ce1f445 100644 --- a/plone/app/discussion/profiles/default/metadata.xml +++ b/plone/app/discussion/profiles/default/metadata.xml @@ -1,5 +1,5 @@ - 101 + 102 profile-plone.app.registry:default diff --git a/plone/app/discussion/tests/functional_test_comments.txt b/plone/app/discussion/tests/functional_test_comments.txt index 8a15209..9e7350c 100644 --- a/plone/app/discussion/tests/functional_test_comments.txt +++ b/plone/app/discussion/tests/functional_test_comments.txt @@ -320,6 +320,146 @@ But Anon can see the edited comment. True +Deleting existing comments | 'delete own comments' disabled +----------------------------------------------------------- + +Anonymous cannot delete comments + + >>> unprivileged_browser.open(urldoc1) + >>> 'form.button.Delete' in unprivileged_browser.contents + False + +A member cannot delete his own comments, unless this is explicitly enabled (see later) + + >>> browser_member.open(urldoc1) + >>> 'form.button.Delete' in browser_member.contents + False + +Admin can delete comments + + >>> browser.open(urldoc1) + >>> 'form.button.Delete' in browser.contents + True + +Extract the delete comment url from the first "delete comment" button + + >>> browser.open(urldoc1) + >>> form = browser.getForm(name='delete', index=0) + >>> delete_url = form.action + >>> '@@moderate-delete-comment' in delete_url + True + >>> comment_id = delete_url.split('/')[-2] + +Anonymous cannot delete a comment by hitting the delete url directly. + + >>> unprivileged_browser.open(delete_url) + +The comment is still there + + >>> unprivileged_browser.open(urldoc1) + >>> comment_id in unprivileged_browser.contents + True + + +A Member cannot delete even his own comment by hitting the delete url directly. + +Extract the member comment id from the admin browser + + >>> form = browser.getForm(name='delete', index=2) + >>> delete_url = form.action + >>> '@@moderate-delete-comment' in delete_url + True + >>> comment_id = delete_url.split('/')[-2] + +Now try to hit that url as the member owning that comment. +Work around some possible testbrowser breakage and check the result later. + + >>> try: + ... browser_member.open(delete_url) + ... except: + ... pass + +The comment is still there + + >>> browser_member.open(urldoc1) + >>> comment_id in browser_member.contents + True + >>> 'Comment from Jim' in browser_member.contents + True + +Admin, who hase 'review comments' permission, can delete comments + + >>> browser.open(urldoc1) + >>> form = browser.getForm(name='delete', index=0) + >>> '@@moderate-delete-comment' in form.action + True + + >>> comment_id = form.action.split('/')[-2] + +Submitting the form runs into a testbrowser notFoundException. +We'll just catch that and check the result later. + + >>> try: + ... form.submit() + ... except: + ... pass + +Returning to the document we find the deleted comment is indeed gone + + >>> browser.open(urldoc1) + >>> comment_id in browser.contents + False + + +Deleting existing comments | 'delete own comments' ENABLED +---------------------------------------------------------- + +Enable deletion of own comments + + >>> from zope.component import queryUtility + >>> from plone.registry.interfaces import IRegistry + >>> from plone.app.discussion.interfaces import IDiscussionSettings + >>> registry = queryUtility(IRegistry) + >>> settings = registry.forInterface(IDiscussionSettings) + >>> settings.delete_own_comment_enabled = True + + >>> import transaction + >>> transaction.commit() + +Anonymous still cannot delete comments + + >>> unprivileged_browser.open(urldoc1) + >>> 'form.button.Delete' in unprivileged_browser.contents + False + +A member can now delete his own comments + + >>> browser_member.open(urldoc1) + >>> 'form.button.Delete' in browser_member.contents + True + + >>> form = browser_member.getForm(name='delete', index=0) + >>> '@@moderate-delete-comment' in form.action + True + + >>> comment_id = form.action.split('/')[-2] + +Submitting the form runs into a testbrowser notFoundException. +We'll just catch that and check the result later. + + >>> try: + ... form.submit() + ... except: + ... pass + +Returning to the document we find the deleted comment is indeed gone + + >>> browser_member.open(urldoc1) + >>> comment_id in browser_member.contents + False + >>> 'Comment from Jim' in browser_member.contents + False + Post a comment with comment review workflow enabled ---------------------------------------------------