Merge pull request #50 from cekk/edit-delete-final

Edit and delete comments
This commit is contained in:
Timo Stollenwerk 2014-05-16 11:14:43 +02:00
commit 7f8be09d82
23 changed files with 596 additions and 19 deletions

View File

@ -17,6 +17,13 @@ Changelog
- Update Traditional Chinese translations. - Update Traditional Chinese translations.
[marr] [marr]
- Make comments editable.
[pjstevns, gyst]
- Provide 'Delete comments' permission to handle comments deletion
[cekk]
2.3.2 (2014-04-05) 2.3.2 (2014-04-05)
------------------ ------------------

View File

@ -1,8 +1,16 @@
from Acquisition import aq_inner, aq_parent from Acquisition import aq_inner, aq_parent
from AccessControl import getSecurityManager
from zope.component import getMultiAdapter
from Products.statusmessages.interfaces import IStatusMessage
from Products.Five.browser import BrowserView from Products.Five.browser import BrowserView
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from plone.app.discussion import PloneAppDiscussionMessageFactory as _
from comments import CommentForm
from z3c.form import button
from plone.z3cform.layout import wrap_form
class View(BrowserView): class View(BrowserView):
"""Comment View. """Comment View.
@ -37,3 +45,64 @@ class View(BrowserView):
url = "%s/view" % url url = "%s/view" % url
self.request.response.redirect('%s#%s' % (url, context.id)) self.request.response.redirect('%s#%s' % (url, context.id))
class EditCommentForm(CommentForm):
"""Form to edit an existing comment."""
ignoreContext = True
id = "edit-comment-form"
label = _(u'edit_comment_form_title', default=u'Edit comment')
def updateWidgets(self):
super(EditCommentForm, self).updateWidgets()
self.widgets['text'].value = self.context.text
# We have to rename the id, otherwise TinyMCE can't initialize
# because there are two textareas with the same id.
self.widgets['text'].id = 'overlay-comment-text'
def _redirect(self, target=''):
if not target:
portal_state = getMultiAdapter((self.context, self.request),
name=u'plone_portal_state')
target = portal_state.portal_url()
self.request.response.redirect(target)
@button.buttonAndHandler(_(u"edit_comment_form_button",
default=u"Edit comment"), name='comment')
def handleComment(self, action):
# Validate form
data, errors = self.extractData()
if errors:
return
# Check permissions
can_edit = getSecurityManager().checkPermission(
'Edit comments',
self.context)
mtool = getToolByName(self.context, 'portal_membership')
if mtool.isAnonymousUser() or not can_edit:
return
# Update text
self.context.text = data['text']
# Redirect to comment
IStatusMessage(self.request).add(_(u'comment_edit_notification',
default="Comment was edited"),
type='info')
return self._redirect(
target=self.action.replace("@@edit-comment", "@@view"))
@button.buttonAndHandler(_(u'cancel_form_button',
default=u'Cancel'), name='cancel')
def handle_cancel(self, action):
IStatusMessage(self.request).add(
_(u'comment_edit_cancel_notification',
default=u'Edit comment cancelled'),
type='info')
return self._redirect(target=self.context.absolute_url())
EditComment = wrap_form(EditCommentForm)
#EOF

View File

@ -1,6 +1,7 @@
<tal:block tal:define="userHasReplyPermission view/can_reply; <tal:block tal:define="userHasReplyPermission view/can_reply;
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;
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);
@ -34,7 +35,9 @@
author_home_url python:view.get_commenter_home_url(username=reply.author_username); author_home_url python:view.get_commenter_home_url(username=reply.author_username);
has_author_link python:author_home_url and not isAnon; has_author_link python:author_home_url and not isAnon;
portrait_url python:view.get_commenter_portrait(reply.author_username); portrait_url python:view.get_commenter_portrait(reply.author_username);
review_state python:wtool.getInfoFor(reply, 'review_state', 'none');" review_state python:wtool.getInfoFor(reply, 'review_state', 'none');
canEdit python:view.can_edit(reply);
canDelete python:view.can_delete(reply)"
tal:attributes="class python:'comment replyTreeLevel'+str(depth)+' state-'+str(review_state); tal:attributes="class python:'comment replyTreeLevel'+str(depth)+' state-'+str(review_state);
id string:${reply/getId}" id string:${reply/getId}"
tal:condition="python:canReview or review_state == 'published'"> tal:condition="python:canReview or review_state == 'published'">
@ -87,7 +90,7 @@
action="" action=""
method="post" method="post"
class="commentactionsform" class="commentactionsform"
tal:condition="canReview" tal:condition="python:canDelete"
tal:attributes="action string:${reply/absolute_url}/@@moderate-delete-comment"> tal:attributes="action string:${reply/absolute_url}/@@moderate-delete-comment">
<input name="form.button.DeleteComment" <input name="form.button.DeleteComment"
class="destructive" class="destructive"
@ -97,6 +100,21 @@
/> />
</form> </form>
<form name="edit"
action=""
method="get"
class="commentactionsform"
tal:condition="python:isEditCommentAllowed and canEdit"
tal:attributes="action string:${reply/absolute_url}/@@edit-comment">
<input name="form.button.EditComment"
class="context"
type="submit"
value="Edit"
i18n:attributes="value label_edit;"
/>
</form>
<!-- Workflow actions (e.g. 'publish') --> <!-- Workflow actions (e.g. 'publish') -->
<form name="" <form name=""
action="" action=""

View File

@ -317,6 +317,20 @@ class CommentsViewlet(ViewletBase):
return getSecurityManager().checkPermission('Review comments', return getSecurityManager().checkPermission('Review comments',
aq_inner(self.context)) aq_inner(self.context))
def can_edit(self, reply):
"""Returns true if current user has the 'Edit comments'
permission.
"""
return getSecurityManager().checkPermission('Edit comments',
aq_inner(reply))
def can_delete(self, reply):
"""By default requires 'Review comments'.
If 'delete own comments' is enabled, requires 'Edit comments'.
"""
return getSecurityManager().checkPermission('Delete comments',
aq_inner(reply))
def is_discussion_allowed(self): def is_discussion_allowed(self):
context = aq_inner(self.context) context = aq_inner(self.context)
return context.restrictedTraverse('@@conversation_view').enabled() return context.restrictedTraverse('@@conversation_view').enabled()
@ -442,6 +456,12 @@ class CommentsViewlet(ViewletBase):
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.anonymous_comments return settings.anonymous_comments
def edit_comment_allowed(self):
# Check if editing comments is allowed in the registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.edit_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

@ -71,13 +71,24 @@
permission="zope2.View" permission="zope2.View"
/> />
<!-- Delete comment view --> <!-- Edit comment view -->
<browser:page
for="plone.app.discussion.interfaces.IComment"
name="edit-comment"
layer="..interfaces.IDiscussionLayer"
class=".comment.EditComment"
permission="plone.app.discussion.EditComments"
/>
<!-- Delete comment view
has conditional security dependent on controlpanel settings.
-->
<browser:page <browser:page
for="plone.app.discussion.interfaces.IComment" for="plone.app.discussion.interfaces.IComment"
name="moderate-delete-comment" name="moderate-delete-comment"
layer="..interfaces.IDiscussionLayer" layer="..interfaces.IDiscussionLayer"
class=".moderation.DeleteComment" class=".moderation.DeleteComment"
permission="plone.app.discussion.ReviewComments" permission="plone.app.discussion.DeleteComments"
/> />
<!-- Publish comment view --> <!-- Publish comment view -->

View File

@ -24,6 +24,7 @@ from z3c.form import button
from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
from plone.app.discussion.interfaces import IDiscussionSettings, _ from plone.app.discussion.interfaces import IDiscussionSettings, _
from plone.app.discussion.upgrades import update_registry
class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
@ -51,6 +52,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
self.fields['moderation_enabled'].widgetFactory = \ self.fields['moderation_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
self.fields['edit_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 = \
@ -61,6 +64,12 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
def updateWidgets(self): def updateWidgets(self):
try:
super(DiscussionSettingsEditForm, self).updateWidgets()
except KeyError:
# upgrade profile not visible in prefs_install_products_form
# provide auto-upgrade
update_registry(self.context)
super(DiscussionSettingsEditForm, self).updateWidgets() super(DiscussionSettingsEditForm, self).updateWidgets()
self.widgets['globally_enabled'].label = _(u"Enable Comments") self.widgets['globally_enabled'].label = _(u"Enable Comments")
self.widgets['anonymous_comments'].label = _(u"Anonymous Comments") self.widgets['anonymous_comments'].label = _(u"Anonymous Comments")
@ -118,6 +127,9 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
elif settings.moderation_enabled: elif settings.moderation_enabled:
output.append("moderation_enabled") output.append("moderation_enabled")
if settings.edit_comment_enabled:
output.append("edit_comment_enabled")
# Anonymous comments # Anonymous comments
if settings.anonymous_comments: if settings.anonymous_comments:
output.append("anonymous_comments") output.append("anonymous_comments")

View File

@ -165,6 +165,15 @@
return false; return false;
}); });
/**********************************************************************
* Edit a comment
**********************************************************************/
$("form[name='edit']").prepOverlay({
cssclass: 'overlay-edit-comment',
width: '60%',
subtype: 'ajax',
filter: '#content>*'
})
/********************************************************************** /**********************************************************************
* Delete a comment and its answers. * Delete a comment and its answers.

View File

@ -40,6 +40,7 @@
$.enableSettings([ $.enableSettings([
$('#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-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'),
@ -52,6 +53,7 @@
$.disableSettings([ $.disableSettings([
$('#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-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

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from Acquisition import aq_inner, aq_parent 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 import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
@ -8,6 +10,8 @@ from Products.CMFCore.utils import getToolByName
from Products.statusmessages.interfaces import IStatusMessage 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 _
from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IComment
@ -94,8 +98,12 @@ class DeleteComment(BrowserView):
comment = aq_inner(self.context) comment = aq_inner(self.context)
conversation = aq_parent(comment) conversation = aq_parent(comment)
content_object = aq_parent(conversation) content_object = aq_parent(conversation)
# 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] del conversation[comment.id]
content_object.reindexObject(idxs=['total_comments']) content_object.reindexObject()
IStatusMessage(self.context.REQUEST).addStatusMessage( IStatusMessage(self.context.REQUEST).addStatusMessage(
_("Comment deleted."), _("Comment deleted."),
type="info") type="info")
@ -105,6 +113,13 @@ class DeleteComment(BrowserView):
came_from = content_object.absolute_url() came_from = content_object.absolute_url()
return self.context.REQUEST.RESPONSE.redirect(came_from) 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'.
"""
return getSecurityManager().checkPermission('Delete comments',
aq_inner(reply))
class PublishComment(BrowserView): class PublishComment(BrowserView):
"""Publish a comment. """Publish a comment.

View File

@ -223,3 +223,9 @@
.row .discussion label { .row .discussion label {
font-weight:bold; font-weight:bold;
} }
/* editing comments */
.overlay-edit-comment textarea {
height: 10em;
}

View File

@ -48,6 +48,7 @@ from Products.CMFCore.CMFCatalogAware import WorkflowAware
from OFS.role import RoleManager from OFS.role import RoleManager
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from AccessControl.SecurityManagement import getSecurityManager
from Products.CMFCore import permissions from Products.CMFCore import permissions
@ -121,6 +122,14 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
self.creation_date = self.modification_date = datetime.utcnow() self.creation_date = self.modification_date = datetime.utcnow()
self.mime_type = 'text/plain' self.mime_type = 'text/plain'
user = getSecurityManager().getUser()
if user and user.getId():
aclpath = [x for x in user.getPhysicalPath() if x]
self._owner = (aclpath, user.getId(),)
self.__ac_local_roles__ = {
user.getId(): ['Owner']
}
@property @property
def __name__(self): def __name__(self):
return self.comment_id and unicode(self.comment_id) or None return self.comment_id and unicode(self.comment_id) or None
@ -199,7 +208,7 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
def Creator(self): def Creator(self):
"""The name of the person who wrote the comment. """The name of the person who wrote the comment.
""" """
return self.creator return self.creator or self.author_name
security.declareProtected(permissions.View, 'Type') security.declareProtected(permissions.View, 'Type')

View File

@ -44,6 +44,27 @@
for="Products.CMFPlone.interfaces.IPloneSiteRoot" for="Products.CMFPlone.interfaces.IPloneSiteRoot"
/> />
<genericsetup:upgradeStep
title="edit comments"
description="reload registry config to enable new field edit_comment_enabled"
source="100"
destination="101"
handler=".upgrades.update_registry"
sortkey="1"
profile="plone.app.discussion:default"
/>
<genericsetup:upgradeStep
title="delete comments"
description="reload rolemap config to enable new permission 'Delete comments'"
source="101"
destination="102"
handler=".upgrades.update_rolemap"
sortkey="1"
profile="plone.app.discussion:default"
/>
<!-- Monkey Patches --> <!-- Monkey Patches -->
<include package="collective.monkeypatcher" /> <include package="collective.monkeypatcher" />

View File

@ -243,6 +243,17 @@ class IDiscussionSettings(Interface):
default=False, default=False,
) )
edit_comment_enabled = schema.Bool(
title=_(u"label_edit_comment_enabled",
default="Enable editing of comments"),
description=_(u"help_edit_comment_enabled",
default=u"If selected, supports editing and deletion "
"of comments for users with the 'Edit 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

@ -9,4 +9,14 @@
title="Review comments" title="Review comments"
/> />
<permission
id="plone.app.discussion.EditComments"
title="Edit comments"
/>
<permission
id="plone.app.discussion.DeleteComments"
title="Delete comments"
/>
</configure> </configure>

View File

@ -1,5 +1,5 @@
<metadata> <metadata>
<version>100</version> <version>102</version>
<dependencies> <dependencies>
<dependency>profile-plone.app.registry:default</dependency> <dependency>profile-plone.app.registry:default</dependency>
</dependencies> </dependencies>

View File

@ -1,4 +1,6 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<registry> <registry>
<records interface="plone.app.discussion.interfaces.IDiscussionSettings" /> <records interface="plone.app.discussion.interfaces.IDiscussionSettings">
<value key="edit_comment_enabled">False</value>
</records>
</registry> </registry>

View File

@ -6,6 +6,17 @@
<role name="Site Administrator"/> <role name="Site Administrator"/>
<role name="Reviewer"/> <role name="Reviewer"/>
</permission> </permission>
<permission name="Edit comments" acquire="True">
<role name="Manager"/>
<role name="Site Administrator"/>
<role name="Reviewer"/>
<role name="Owner"/>
</permission>
<permission name="Delete comments" acquire="True">
<role name="Manager"/>
<role name="Site Administrator"/>
<role name="Reviewer"/>
</permission>
<permission name="Reply to item" acquire="False"> <permission name="Reply to item" acquire="False">
<role name="Authenticated"/> <role name="Authenticated"/>
</permission> </permission>

View File

@ -34,6 +34,8 @@ class PloneAppDiscussion(PloneSandboxLayer):
USER_WITH_FULLNAME_PASSWORD = 'secret' USER_WITH_FULLNAME_PASSWORD = 'secret'
MANAGER_USER_NAME = 'manager' MANAGER_USER_NAME = 'manager'
MANAGER_USER_PASSWORD = 'secret' MANAGER_USER_PASSWORD = 'secret'
REVIEWER_NAME = 'reviewer'
REVIEWER_PASSWORD = 'secret'
def setUpZope(self, app, configurationContext): def setUpZope(self, app, configurationContext):
# Load ZCML # Load ZCML
@ -66,7 +68,15 @@ class PloneAppDiscussion(PloneSandboxLayer):
['Member'], ['Member'],
[], [],
) )
acl_users.userFolderAddUser(
self.REVIEWER_NAME,
self.REVIEWER_PASSWORD,
['Member'],
[],
)
mtool = getToolByName(portal, 'portal_membership', None) mtool = getToolByName(portal, 'portal_membership', None)
gtool = getToolByName(portal, 'portal_groups', None)
gtool.addPrincipalToGroup(self.REVIEWER_NAME, 'Reviewers')
mtool.addMember('jim', 'Jim', ['Member'], []) mtool.addMember('jim', 'Jim', ['Member'], [])
mtool.getMemberById('jim').setMemberProperties( mtool.getMemberById('jim').setMemberProperties(
{"fullname": 'Jim Fult\xc3\xb8rn'}) {"fullname": 'Jim Fult\xc3\xb8rn'})

View File

@ -32,6 +32,7 @@ you're not logged in::
>>> unprivileged_browser = Browser(app) >>> unprivileged_browser = Browser(app)
>>> browser_member = Browser(app) >>> browser_member = Browser(app)
>>> browser_user = Browser(app) >>> browser_user = Browser(app)
>>> browser_reviewer = Browser(app)
Make sure we have a test user from the layer and it uses fancy characters: Make sure we have a test user from the layer and it uses fancy characters:
@ -143,7 +144,6 @@ Post a comment as member
------------------------ ------------------------
Login as user 'jim'. Login as user 'jim'.
>>> browser_member.open(portal_url + '/login_form') >>> browser_member.open(portal_url + '/login_form')
>>> browser_member.getControl(name='__ac_name').value = 'jim' >>> browser_member.getControl(name='__ac_name').value = 'jim'
>>> browser_member.getControl(name='__ac_password').value = 'secret' >>> browser_member.getControl(name='__ac_password').value = 'secret'
@ -250,6 +250,173 @@ Check that the reply has been posted properly.
True True
Edit an existing comment
------------------------
Log in as admin
>>> browser.getLink('Log out').click()
>>> browser.open(portal_url + '/login_form')
>>> browser.getControl('Login Name').value = 'admin'
>>> browser.getControl('Password').value = 'secret'
>>> browser.getControl('Log in').click()
Use the Plone control panel to enable comment editing.
>>> browser.open(portal_url + '/plone_control_panel')
>>> browser.getLink('Discussion').click()
>>> browser.getControl('Enable editing of comments').selected = True
>>> browser.getControl(name='form.buttons.save').click()
Extract the edit comment url from the first "edit comment" button
>>> browser.open(urldoc1)
>>> form = browser.getForm(name='edit', index=0)
>>> '@@edit-comment' in form.action
True
Open the edit comment view
>>> browser.open(form.action)
>>> ctrl = browser.getControl('Comment')
>>> ctrl.value
'Comment from admin'
Change and save the comment
>>> ctrl.value = 'Comment from admin / was edited'
>>> browser.getControl('Edit comment').click()
This used to trigger permissions problems in some portlet configurations.
Check it ain't so.
>>> 'require_login' in browser.url
False
>>> browser.url.startswith('http://nohost/plone/doc1')
True
>>> 'Comment from admin / was edited' in browser.contents
True
Opening the edit comment view, then cancel, does nothing.
>>> form = browser.getForm(name='edit', index=0)
>>> '@@edit-comment' in form.action
True
>>> browser.open(form.action)
>>> browser.getControl('Cancel').click()
>>> browser.url.startswith('http://nohost/plone/doc1')
True
Anon cannot edit comments.
>>> unprivileged_browser.open(urldoc1)
>>> '@@edit-comments' in browser.contents
False
But Anon can see the edited comment.
>>> 'Comment from admin / was edited' in unprivileged_browser.contents
True
Deleting existing comments | 'Delete comments' permission
----------------------------------------------------------
Anonymous cannot delete comments
>>> unprivileged_browser.open(urldoc1)
>>> 'form.button.Delete' in unprivileged_browser.contents
False
A member cannot delete his own comments if he can't review or he isn't a Site Administrator
>>> 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
Now login as user 'reviewer'
>>> browser_reviewer.open(portal_url + '/login_form')
>>> browser_reviewer.getControl(name='__ac_name').value = 'reviewer'
>>> browser_reviewer.getControl(name='__ac_password').value = 'secret'
>>> browser_reviewer.getControl(name='submit').click()
Admin and who have 'Delete comments' permission (reviewers for example), can delete comments
>>> browser_reviewer.open(urldoc1)
>>> form = browser_reviewer.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_reviewer.open(urldoc1)
>>> comment_id in browser_reviewer.contents
False
Post a comment with comment review workflow enabled Post a comment with comment review workflow enabled
--------------------------------------------------- ---------------------------------------------------

View File

@ -142,6 +142,16 @@ class CommentTest(unittest.TestCase):
comment1.creator = "jim" comment1.creator = "jim"
self.assertEqual("jim", comment1.Creator()) self.assertEqual("jim", comment1.Creator())
def test_creator_author_name(self):
comment1 = createObject('plone.Comment')
comment1.author_name = "joey"
self.assertEqual("joey", comment1.Creator())
def test_owner(self):
comment1 = createObject('plone.Comment')
self.assertEqual((['plone', 'acl_users'], TEST_USER_ID),
comment1.getOwnerTuple())
def test_type(self): def test_type(self):
comment1 = createObject('plone.Comment') comment1 = createObject('plone.Comment')
self.assertEqual(comment1.Type(), 'Comment') self.assertEqual(comment1.Type(), 'Comment')

View File

@ -33,12 +33,14 @@ from plone.app.testing import login
from plone.app.discussion.browser.comments import CommentsViewlet from plone.app.discussion.browser.comments import CommentsViewlet
from plone.app.discussion.browser.comments import CommentForm from plone.app.discussion.browser.comments import CommentForm
from plone.app.discussion.browser.comment import EditCommentForm
from plone.app.discussion import interfaces from plone.app.discussion import interfaces
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.testing import ( from plone.app.discussion.testing import (
PLONE_APP_DISCUSSION_INTEGRATION_TESTING PLONE_APP_DISCUSSION_INTEGRATION_TESTING
) )
from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IConversation
class TestCommentForm(unittest.TestCase): class TestCommentForm(unittest.TestCase):
@ -135,6 +137,135 @@ class TestCommentForm(unittest.TestCase):
self.assertEqual(len(roles), 1) self.assertEqual(len(roles), 1)
self.assertEqual(roles[0], 'Owner') self.assertEqual(roles[0], 'Owner')
def test_edit_comment(self):
"""Edit a 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"
)
provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=EditCommentForm,
name=u"edit-comment-form"
)
# The form is submitted successfully, if the required text field is
# filled out
request = make_request(form={'form.widgets.text': u'bar'})
commentForm = getMultiAdapter(
(self.context, 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"))
# Edit the last comment
conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1]
request = make_request(form={'form.widgets.text': u'foobar'})
editForm = getMultiAdapter(
(comment, request),
name=u"edit-comment-form"
)
editForm.update()
data, errors = editForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(editForm.handleComment(editForm, "foo"))
comment = [x for x in conversation.getComments()][-1]
self.assertEquals(comment.text, u"foobar")
comments = IConversation(commentForm.context).getComments()
comments = [comment for comment in comments] # consume itertor
self.assertEqual(len(comments), 1)
for comment in comments:
self.assertEqual(comment.text, u"foobar")
self.assertEqual(comment.creator, "test_user_1_")
self.assertEqual(comment.getOwner().getUserName(), "test-user")
local_roles = comment.get_local_roles()
self.assertEqual(len(local_roles), 1)
userid, roles = local_roles[0]
self.assertEqual(userid, 'test_user_1_')
self.assertEqual(len(roles), 1)
self.assertEqual(roles[0], 'Owner')
def test_delete_comment(self):
"""Delete a 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"moderate-delete-comment"
)
# try to delete last comment without "Delete comments" permission
setRoles(self.portal, TEST_USER_ID, ['Member'])
self.assertRaises(Unauthorized, comment.restrictedTraverse, "@@moderate-delete-comment")
deleteView()
self.assertEqual(1, len([x for x in conversation.getComments()]))
# try to delete last comment with "Delete comments" permission
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
deleteView()
self.assertEqual(0, len([x for x in conversation.getComments()]))
setRoles(self.portal, TEST_USER_ID, ['Manager'])
def test_add_anonymous_comment(self): def test_add_anonymous_comment(self):
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True
@ -477,9 +608,13 @@ class TestCommentsViewlet(unittest.TestCase):
) )
def test_get_commenter_portrait_is_none(self): def test_get_commenter_portrait_is_none(self):
self.assertEqual(
self.viewlet.get_commenter_portrait(), self.assertTrue(
'defaultUser.png' self.viewlet.get_commenter_portrait() in (
'defaultUser.png',
'defaultUser.gif',
)
) )
def test_get_commenter_portrait_without_userimage(self): def test_get_commenter_portrait_without_userimage(self):

View File

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

View File

@ -2,7 +2,13 @@ from zope.component import getUtility
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import IDiscussionSettings
default_profile = 'profile-plone.app.discussion:default'
def update_registry(context): def update_registry(context):
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
registry.registerInterface(IDiscussionSettings) registry.registerInterface(IDiscussionSettings)
def update_rolemap(context):
context.runImportStepFromProfile(default_profile, 'rolemap')