Merge branch 'delete-own-comments'

This commit is contained in:
Timo Stollenwerk 2015-01-29 21:19:50 +01:00
commit d7feb46bcb
16 changed files with 189 additions and 17 deletions

View File

@ -4,6 +4,10 @@ Changelog
2.3.4 (unreleased) 2.3.4 (unreleased)
------------------ ------------------
- Add permission to allow comment authors to delete their own comments if
there are no replies yet.
[gaudenz]
- Updated portuguese pt-br translation. - Updated portuguese pt-br translation.
[jtmolon] [jtmolon]

View File

@ -2,6 +2,7 @@
isDiscussionAllowed view/is_discussion_allowed; isDiscussionAllowed view/is_discussion_allowed;
isAnonymousDiscussionAllowed view/anonymous_discussion_allowed; isAnonymousDiscussionAllowed view/anonymous_discussion_allowed;
isEditCommentAllowed view/edit_comment_allowed; isEditCommentAllowed view/edit_comment_allowed;
isDeleteOwnCommentAllowed view/delete_own_comment_allowed;
isAnon view/is_anonymous; isAnon view/is_anonymous;
canReview view/can_review; canReview view/can_review;
replies python:view.get_replies(canReview); replies python:view.get_replies(canReview);
@ -86,6 +87,20 @@
<span tal:replace="structure reply/getText" /> <span tal:replace="structure reply/getText" />
<div class="commentActions"> <div class="commentActions">
<form name="delete"
action=""
method="post"
class="commentactionsform"
tal:condition="python:not canDelete and isDeleteOwnCommentAllowed and view.could_delete_own(reply)"
tal:attributes="action string:${reply/absolute_url}/@@delete-own-comment;
style python:view.can_delete_own(reply) and 'display: inline' or 'display: none'">
<input name="form.button.DeleteComment"
class="destructive"
type="submit"
value="Delete"
i18n:attributes="value label_delete;"
/>
</form>
<form name="delete" <form name="delete"
action="" action=""
method="post" method="post"

View File

@ -230,6 +230,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
comment.creation_date = datetime.utcnow() comment.creation_date = datetime.utcnow()
comment.modification_date = datetime.utcnow() comment.modification_date = datetime.utcnow()
else: # pragma: no cover else: # pragma: no cover
raise Unauthorized( raise Unauthorized(
u"Anonymous user tries to post a comment, but anonymous " u"Anonymous user tries to post a comment, but anonymous "
@ -317,6 +318,26 @@ class CommentsViewlet(ViewletBase):
return getSecurityManager().checkPermission('Review comments', return getSecurityManager().checkPermission('Review comments',
aq_inner(self.context)) aq_inner(self.context))
def can_delete_own(self, comment):
"""Returns true if the current user can delete the comment. Only
comments without replies can be deleted.
"""
try:
return comment.restrictedTraverse(
'@@delete-own-comment').can_delete()
except Unauthorized:
return False
def could_delete_own(self, comment):
"""Returns true if the current user could delete the comment if it had
no replies. This is used to prepare hidden form buttons for JS.
"""
try:
return comment.restrictedTraverse(
'@@delete-own-comment').could_delete()
except Unauthorized:
return False
def can_edit(self, reply): def can_edit(self, reply):
"""Returns true if current user has the 'Edit comments' """Returns true if current user has the 'Edit comments'
permission. permission.
@ -325,8 +346,8 @@ class CommentsViewlet(ViewletBase):
aq_inner(reply)) aq_inner(reply))
def can_delete(self, reply): def can_delete(self, reply):
"""By default requires 'Review comments'. """Returns true if current user has the 'Delete comments'
If 'delete own comments' is enabled, requires 'Edit comments'. permission.
""" """
return getSecurityManager().checkPermission('Delete comments', return getSecurityManager().checkPermission('Delete comments',
aq_inner(reply)) aq_inner(reply))
@ -397,7 +418,6 @@ class CommentsViewlet(ViewletBase):
return iter([]) return iter([])
wf = getToolByName(context, 'portal_workflow') wf = getToolByName(context, 'portal_workflow')
# workflow_actions is only true when user # workflow_actions is only true when user
# has 'Manage portal' permission # has 'Manage portal' permission
@ -462,6 +482,12 @@ class CommentsViewlet(ViewletBase):
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.edit_comment_enabled return settings.edit_comment_enabled
def delete_own_comment_allowed(self):
# Check if delete own comments is allowed in the registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.delete_own_comment_enabled
def show_commenter_image(self): def show_commenter_image(self):
# Check if showing commenter image is enabled in the registry # Check if showing commenter image is enabled in the registry
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)

View File

@ -80,7 +80,7 @@
permission="plone.app.discussion.EditComments" permission="plone.app.discussion.EditComments"
/> />
<!-- Delete comment view <!-- Delete comment views
has conditional security dependent on controlpanel settings. has conditional security dependent on controlpanel settings.
--> -->
<browser:page <browser:page
@ -91,6 +91,14 @@
permission="plone.app.discussion.DeleteComments" permission="plone.app.discussion.DeleteComments"
/> />
<browser:page
for="plone.app.discussion.interfaces.IComment"
name="delete-own-comment"
layer="..interfaces.IDiscussionLayer"
class=".moderation.DeleteOwnComment"
permission="plone.app.discussion.DeleteOwnComments"
/>
<!-- Publish comment view --> <!-- Publish comment view -->
<browser:page <browser:page
for="plone.app.discussion.interfaces.IComment" for="plone.app.discussion.interfaces.IComment"

View File

@ -52,6 +52,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
self.fields['edit_comment_enabled'].widgetFactory = \ self.fields['edit_comment_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
self.fields['delete_own_comment_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['anonymous_comments'].widgetFactory = \ self.fields['anonymous_comments'].widgetFactory = \
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
self.fields['show_commenter_image'].widgetFactory = \ self.fields['show_commenter_image'].widgetFactory = \
@ -128,6 +130,9 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
if settings.edit_comment_enabled: if settings.edit_comment_enabled:
output.append("edit_comment_enabled") output.append("edit_comment_enabled")
if settings.delete_own_comment_enabled:
output.append("delte_own_comment_enabled")
# Anonymous comments # Anonymous comments
if settings.anonymous_comments: if settings.anonymous_comments:
output.append("anonymous_comments") output.append("anonymous_comments")

View File

@ -211,6 +211,9 @@
$(this).remove(); $(this).remove();
}); });
}); });
// Add delete button to the parent
var parent = comment.prev('[class*="replyTreeLevel' + (treelevel - 1) + '"]');
parent.find('form[name="delete"]').css('display', 'inline');
// remove comment // remove comment
$(this).fadeOut('fast', function () { $(this).fadeOut('fast', function () {
$(this).remove(); $(this).remove();

View File

@ -41,6 +41,7 @@
$('#formfield-form-widgets-anonymous_comments'), $('#formfield-form-widgets-anonymous_comments'),
$('#formfield-form-widgets-moderation_enabled'), $('#formfield-form-widgets-moderation_enabled'),
$('#formfield-form-widgets-edit_comment_enabled'), $('#formfield-form-widgets-edit_comment_enabled'),
$('#formfield-form-widgets-delete_own_comment_enabled'),
$('#formfield-form-widgets-text_transform'), $('#formfield-form-widgets-text_transform'),
$('#formfield-form-widgets-captcha'), $('#formfield-form-widgets-captcha'),
$('#formfield-form-widgets-show_commenter_image'), $('#formfield-form-widgets-show_commenter_image'),
@ -54,6 +55,7 @@
$('#formfield-form-widgets-anonymous_comments'), $('#formfield-form-widgets-anonymous_comments'),
$('#formfield-form-widgets-moderation_enabled'), $('#formfield-form-widgets-moderation_enabled'),
$('#formfield-form-widgets-edit_comment_enabled'), $('#formfield-form-widgets-edit_comment_enabled'),
$('#formfield-form-widgets-delete_own_comment_enabled'),
$('#formfield-form-widgets-text_transform'), $('#formfield-form-widgets-text_transform'),
$('#formfield-form-widgets-captcha'), $('#formfield-form-widgets-captcha'),
$('#formfield-form-widgets-show_commenter_image'), $('#formfield-form-widgets-show_commenter_image'),

View File

@ -3,6 +3,8 @@ from Acquisition import aq_inner, aq_parent
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
from zope.component import queryUtility from zope.component import queryUtility
from AccessControl import Unauthorized, getSecurityManager
from Products.Five.browser import BrowserView from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
@ -14,6 +16,7 @@ from plone.registry.interfaces import IRegistry
from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import _ from plone.app.discussion.interfaces import _
from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IReplies
class View(BrowserView): class View(BrowserView):
@ -114,13 +117,42 @@ class DeleteComment(BrowserView):
return self.context.REQUEST.RESPONSE.redirect(came_from) return self.context.REQUEST.RESPONSE.redirect(came_from)
def can_delete(self, reply): def can_delete(self, reply):
"""By default requires 'Review comments'. """Returns true if current user has the 'Delete comments'
If 'delete own comments' is enabled, requires 'Edit comments'. permission.
""" """
return getSecurityManager().checkPermission('Delete comments', return getSecurityManager().checkPermission('Delete comments',
aq_inner(reply)) aq_inner(reply))
class DeleteOwnComment(DeleteComment):
"""Delete an own comment if it has no replies. Following conditions have to be true
for a user to be able to delete his comments:
* "Delete own comments" permission
* no replies to the comment
* Owner role directly assigned on the comment object
"""
def could_delete(self, comment=None):
"""Returns true if the comment could be deleted if it had no replies."""
sm = getSecurityManager()
comment = comment or aq_inner(self.context)
userid = sm.getUser().getId()
return (sm.checkPermission('Delete own comments',
comment)
and 'Owner' in comment.get_local_roles_for_userid(userid))
def can_delete(self, comment=None):
comment = comment or self.context
return (len(IReplies(aq_inner(comment))) == 0
and self.could_delete(comment=comment))
def __call__(self):
if self.can_delete():
super(DeleteOwnComment, self).__call__()
else:
raise Unauthorized("You're not allowed to delete this comment.")
class PublishComment(BrowserView): class PublishComment(BrowserView):
"""Publish a comment. """Publish a comment.

View File

@ -45,8 +45,8 @@
/> />
<genericsetup:upgradeStep <genericsetup:upgradeStep
title="edit comments" title="edit comments and delete own comments"
description="reload registry config to enable new field edit_comment_enabled" description="reload registry config to enable new fields edit_comment_enabled and delete_own_comment_enabled"
source="100" source="100"
destination="101" destination="101"
handler=".upgrades.update_registry" handler=".upgrades.update_registry"
@ -55,8 +55,8 @@
/> />
<genericsetup:upgradeStep <genericsetup:upgradeStep
title="delete comments" title="delete comments and delete own comments"
description="reload rolemap config to enable new permission 'Delete comments'" description="reload rolemap config to enable new permissions 'Delete comments' and 'Delete own comments'"
source="101" source="101"
destination="102" destination="102"
handler=".upgrades.update_rolemap" handler=".upgrades.update_rolemap"

View File

@ -247,13 +247,24 @@ class IDiscussionSettings(Interface):
title=_(u"label_edit_comment_enabled", title=_(u"label_edit_comment_enabled",
default="Enable editing of comments"), default="Enable editing of comments"),
description=_(u"help_edit_comment_enabled", description=_(u"help_edit_comment_enabled",
default=u"If selected, supports editing and deletion " default=u"If selected, supports editing "
"of comments for users with the 'Edit comments' " "of comments for users with the 'Edit comments' "
"permission."), "permission."),
required=False, required=False,
default=False, default=False,
) )
delete_own_comment_enabled = schema.Bool(
title=_(u"label_delete_own_comment_enabled",
default="Enable deleting own comments"),
description=_(u"help_delete_own_comment_enabled",
default=u"If selected, supports deleting "
"of own comments for users with the "
"'Delete own comments' permission."),
required=False,
default=False,
)
text_transform = schema.Choice( text_transform = schema.Choice(
title=_(u"label_text_transform", title=_(u"label_text_transform",
default="Comment text transform"), default="Comment text transform"),

View File

@ -14,6 +14,11 @@
title="Edit comments" title="Edit comments"
/> />
<permission
id="plone.app.discussion.DeleteOwnComments"
title="Delete own comments"
/>
<permission <permission
id="plone.app.discussion.DeleteComments" id="plone.app.discussion.DeleteComments"
title="Delete comments" title="Delete comments"

View File

@ -2,5 +2,6 @@
<registry> <registry>
<records interface="plone.app.discussion.interfaces.IDiscussionSettings"> <records interface="plone.app.discussion.interfaces.IDiscussionSettings">
<value key="edit_comment_enabled">False</value> <value key="edit_comment_enabled">False</value>
<value key="delete_own_comment_enabled">False</value>
</records> </records>
</registry> </registry>

View File

@ -20,5 +20,11 @@
<permission name="Reply to item" acquire="False"> <permission name="Reply to item" acquire="False">
<role name="Authenticated"/> <role name="Authenticated"/>
</permission> </permission>
<permission name="Delete own comments" acquire="False">
<role name="Manager"/>
<role name="Site Administrator"/>
<role name="Reviewer"/>
<role name="Owner"/>
</permission>
</permissions> </permissions>
</rolemap> </rolemap>

View File

@ -26,7 +26,7 @@ from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.tests import dummy from Products.CMFPlone.tests import dummy
from plone.app.testing import TEST_USER_ID, setRoles from plone.app.testing import TEST_USER_ID, TEST_USER_NAME, setRoles
from plone.app.testing import logout from plone.app.testing import logout
from plone.app.testing import login from plone.app.testing import login
@ -266,6 +266,60 @@ class TestCommentForm(unittest.TestCase):
self.assertEqual(0, len([x for x in conversation.getComments()])) self.assertEqual(0, len([x for x in conversation.getComments()]))
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ['Manager'])
def test_delete_own_comment(self):
"""Delete own comment as logged-in user.
"""
# Allow discussion
self.portal.doc1.allow_discussion = True
self.viewlet = CommentsViewlet(self.context, self.request, None, None)
def make_request(form={}):
request = TestRequest()
request.form.update(form)
alsoProvides(request, IFormLayer)
alsoProvides(request, IAttributeAnnotatable)
return request
provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u"comment-form"
)
# The form is submitted successfully, if the required text field is
# filled out
form_request = make_request(form={'form.widgets.text': u'bar'})
commentForm = getMultiAdapter(
(self.context, form_request),
name=u"comment-form"
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Delete the last comment
conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1]
deleteView = getMultiAdapter(
(comment, self.request),
name=u"delete-own-comment"
)
# try to delete last comment with johndoe
setRoles(self.portal, 'johndoe', ['Member'])
login(self.portal, 'johndoe')
self.assertRaises(Unauthorized, comment.restrictedTraverse, "@@delete-own-comment")
self.assertEqual(1, len([x for x in conversation.getComments()]))
# try to delete last comment with the same user that created it
login(self.portal, TEST_USER_NAME)
setRoles(self.portal, TEST_USER_ID, ['Member'])
deleteView()
self.assertEqual(0, len([x for x in conversation.getComments()]))
def test_add_anonymous_comment(self): def test_add_anonymous_comment(self):
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True

View File

@ -89,12 +89,12 @@ class RegistryTest(unittest.TestCase):
'IDiscussionSettings.edit_comment_enabled'], 'IDiscussionSettings.edit_comment_enabled'],
False) False)
def test_edit_comment_enabled(self): def test_delete_own_comment_enabled(self):
# Check edit_comment_enabled record # Check delete_own_comment_enabled record
self.assertTrue('edit_comment_enabled' in IDiscussionSettings) self.assertTrue('delete_own_comment_enabled' in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry['plone.app.discussion.interfaces.' + self.registry['plone.app.discussion.interfaces.' +
'IDiscussionSettings.edit_comment_enabled'], 'IDiscussionSettings.delete_own_comment_enabled'],
False) False)
def test_text_transform(self): def test_text_transform(self):

View File

@ -56,7 +56,7 @@ setup(name='plone.app.discussion',
'plone.contentrules', 'plone.contentrules',
'plone.app.contentrules', 'plone.app.contentrules',
'plone.app.contenttypes[test]', 'plone.app.contenttypes[test]',
'plone.app.robotframework[ride,reload,debug]', 'plone.app.robotframework[ride,reload]',
] ]
}, },
entry_points=""" entry_points="""