diff --git a/CHANGES.rst b/CHANGES.rst index 6790b6e..27b7716 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,7 @@ Changelog ========= + 2.3.3 (unreleased) ------------------ @@ -17,6 +18,10 @@ Changelog - Update Traditional Chinese translations. [marr] +- Make comments editable. + [pjstevns, gyst] + + 2.3.2 (2014-04-05) ------------------ diff --git a/plone/app/discussion/browser/comment.py b/plone/app/discussion/browser/comment.py index 97eec23..03e0732 100644 --- a/plone/app/discussion/browser/comment.py +++ b/plone/app/discussion/browser/comment.py @@ -1,8 +1,16 @@ 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.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): """Comment View. @@ -37,3 +45,64 @@ class View(BrowserView): url = "%s/view" % url 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 diff --git a/plone/app/discussion/browser/comments.pt b/plone/app/discussion/browser/comments.pt index 4e32e99..203e813 100644 --- a/plone/app/discussion/browser/comments.pt +++ b/plone/app/discussion/browser/comments.pt @@ -1,6 +1,7 @@ @@ -87,7 +89,7 @@ action="" method="post" class="commentactionsform" - tal:condition="canReview" + tal:condition="python:canReview" tal:attributes="action string:${reply/absolute_url}/@@moderate-delete-comment"> +
+ +
+ +
+ + + *' + }) /********************************************************************** * Delete a comment and its answers. diff --git a/plone/app/discussion/browser/javascripts/controlpanel.js b/plone/app/discussion/browser/javascripts/controlpanel.js index f821bf9..e379f6f 100644 --- a/plone/app/discussion/browser/javascripts/controlpanel.js +++ b/plone/app/discussion/browser/javascripts/controlpanel.js @@ -40,6 +40,7 @@ $.enableSettings([ $('#formfield-form-widgets-anonymous_comments'), $('#formfield-form-widgets-moderation_enabled'), + $('#formfield-form-widgets-edit_comment_enabled'), $('#formfield-form-widgets-text_transform'), $('#formfield-form-widgets-captcha'), $('#formfield-form-widgets-show_commenter_image'), @@ -52,6 +53,7 @@ $.disableSettings([ $('#formfield-form-widgets-anonymous_comments'), $('#formfield-form-widgets-moderation_enabled'), + $('#formfield-form-widgets-edit_comment_enabled'), $('#formfield-form-widgets-text_transform'), $('#formfield-form-widgets-captcha'), $('#formfield-form-widgets-show_commenter_image'), diff --git a/plone/app/discussion/browser/stylesheets/discussion.css b/plone/app/discussion/browser/stylesheets/discussion.css index a8ca7a5..3857836 100644 --- a/plone/app/discussion/browser/stylesheets/discussion.css +++ b/plone/app/discussion/browser/stylesheets/discussion.css @@ -223,3 +223,9 @@ .row .discussion label { font-weight:bold; } + +/* editing comments */ + +.overlay-edit-comment textarea { + height: 10em; +} \ No newline at end of file diff --git a/plone/app/discussion/comment.py b/plone/app/discussion/comment.py index bf41967..193f30e 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -48,6 +48,7 @@ from Products.CMFCore.CMFCatalogAware import WorkflowAware from OFS.role import RoleManager from AccessControl import ClassSecurityInfo +from AccessControl.SecurityManagement import getSecurityManager from Products.CMFCore import permissions @@ -121,6 +122,14 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, self.creation_date = self.modification_date = datetime.utcnow() 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 def __name__(self): return self.comment_id and unicode(self.comment_id) or None @@ -199,7 +208,7 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, def Creator(self): """The name of the person who wrote the comment. """ - return self.creator + return self.creator or self.author_name security.declareProtected(permissions.View, 'Type') diff --git a/plone/app/discussion/configure.zcml b/plone/app/discussion/configure.zcml index c3345f7..0a689b1 100644 --- a/plone/app/discussion/configure.zcml +++ b/plone/app/discussion/configure.zcml @@ -44,6 +44,17 @@ for="Products.CMFPlone.interfaces.IPloneSiteRoot" /> + + + diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index c39b8c5..a807580 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -243,6 +243,17 @@ class IDiscussionSettings(Interface): 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( title=_(u"label_text_transform", default="Comment text transform"), diff --git a/plone/app/discussion/permissions.zcml b/plone/app/discussion/permissions.zcml index c5b090c..dd71e63 100644 --- a/plone/app/discussion/permissions.zcml +++ b/plone/app/discussion/permissions.zcml @@ -9,4 +9,10 @@ title="Review comments" /> + + + diff --git a/plone/app/discussion/profiles/default/metadata.xml b/plone/app/discussion/profiles/default/metadata.xml index ffedb11..7a20473 100644 --- a/plone/app/discussion/profiles/default/metadata.xml +++ b/plone/app/discussion/profiles/default/metadata.xml @@ -1,5 +1,5 @@ - 100 + 101 profile-plone.app.registry:default diff --git a/plone/app/discussion/profiles/default/registry.xml b/plone/app/discussion/profiles/default/registry.xml index fd2a894..2518c9a 100644 --- a/plone/app/discussion/profiles/default/registry.xml +++ b/plone/app/discussion/profiles/default/registry.xml @@ -1,4 +1,6 @@ - - \ No newline at end of file + + False + + diff --git a/plone/app/discussion/profiles/default/rolemap.xml b/plone/app/discussion/profiles/default/rolemap.xml index 83318d8..8e8b019 100644 --- a/plone/app/discussion/profiles/default/rolemap.xml +++ b/plone/app/discussion/profiles/default/rolemap.xml @@ -6,6 +6,12 @@ + + + + + + diff --git a/plone/app/discussion/tests/functional_test_comments.txt b/plone/app/discussion/tests/functional_test_comments.txt index 55a489d..8a15209 100644 --- a/plone/app/discussion/tests/functional_test_comments.txt +++ b/plone/app/discussion/tests/functional_test_comments.txt @@ -250,6 +250,77 @@ Check that the reply has been posted properly. 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 + + + Post a comment with comment review workflow enabled --------------------------------------------------- diff --git a/plone/app/discussion/tests/test_comment.py b/plone/app/discussion/tests/test_comment.py index d3c25e0..5277f5f 100644 --- a/plone/app/discussion/tests/test_comment.py +++ b/plone/app/discussion/tests/test_comment.py @@ -142,6 +142,16 @@ class CommentTest(unittest.TestCase): comment1.creator = "jim" 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): comment1 = createObject('plone.Comment') self.assertEqual(comment1.Type(), 'Comment') diff --git a/plone/app/discussion/tests/test_comments_viewlet.py b/plone/app/discussion/tests/test_comments_viewlet.py index c0d45b2..e469b91 100644 --- a/plone/app/discussion/tests/test_comments_viewlet.py +++ b/plone/app/discussion/tests/test_comments_viewlet.py @@ -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 CommentForm +from plone.app.discussion.browser.comment import EditCommentForm from plone.app.discussion import interfaces from plone.app.discussion.interfaces import IConversation from plone.app.discussion.testing import ( PLONE_APP_DISCUSSION_INTEGRATION_TESTING ) from plone.app.discussion.interfaces import IDiscussionSettings +from plone.app.discussion.interfaces import IConversation class TestCommentForm(unittest.TestCase): @@ -135,6 +137,65 @@ class TestCommentForm(unittest.TestCase): self.assertEqual(len(roles), 1) self.assertEqual(roles[0], 'Owner') + def test_edit_comment(self): + """Edit a comment as logged-in user. + """ + + # Allow discussion + self.discussionTool.overrideDiscussionFor(self.portal.doc1, 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") + def test_add_anonymous_comment(self): self.portal.doc1.allow_discussion = True @@ -477,9 +538,13 @@ class TestCommentsViewlet(unittest.TestCase): ) def test_get_commenter_portrait_is_none(self): - self.assertEqual( - self.viewlet.get_commenter_portrait(), - 'defaultUser.png' + + self.assertTrue( + self.viewlet.get_commenter_portrait() in ( + 'defaultUser.png', + 'defaultUser.gif', + ) + ) def test_get_commenter_portrait_without_userimage(self): diff --git a/plone/app/discussion/tests/test_controlpanel.py b/plone/app/discussion/tests/test_controlpanel.py index 40d4540..8a2ba47 100644 --- a/plone/app/discussion/tests/test_controlpanel.py +++ b/plone/app/discussion/tests/test_controlpanel.py @@ -81,6 +81,22 @@ class RegistryTest(unittest.TestCase): 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): self.assertTrue('text_transform' in IDiscussionSettings) self.assertEqual(