From e6172a219e6b3921eef74796a9ee89a97aa1cf61 Mon Sep 17 00:00:00 2001 From: "Guido A.J. Stevens" Date: Tue, 17 Sep 2013 12:03:46 +0000 Subject: [PATCH] Make comments editable --- CHANGES.rst | 4 ++ plone/app/discussion/browser/comment.py | 69 ++++++++++++++++++ plone/app/discussion/browser/comments.pt | 25 +++++-- plone/app/discussion/browser/comments.py | 15 +++- plone/app/discussion/browser/configure.zcml | 9 +++ plone/app/discussion/browser/controlpanel.py | 14 +++- .../browser/javascripts/comments.js | 9 +++ .../browser/javascripts/controlpanel.js | 2 + .../browser/stylesheets/discussion.css | 6 ++ plone/app/discussion/comment.py | 11 ++- plone/app/discussion/configure.zcml | 11 +++ plone/app/discussion/interfaces.py | 11 +++ plone/app/discussion/permissions.zcml | 6 ++ .../discussion/profiles/default/metadata.xml | 2 +- .../discussion/profiles/default/registry.xml | 6 +- .../discussion/profiles/default/rolemap.xml | 6 ++ .../tests/functional_test_comments.txt | 71 +++++++++++++++++++ plone/app/discussion/tests/test_comment.py | 10 +++ .../discussion/tests/test_comments_viewlet.py | 69 +++++++++++++++++- .../app/discussion/tests/test_controlpanel.py | 16 +++++ 20 files changed, 359 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5d9f16f..fa12098 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,13 @@ Changelog ========= + 2.2.9 (unreleased) ------------------ +- Make comments editable. + [pjstevns, gyst] + - Rename CHANGES.txt to CHANGES.rst. [timo] 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 c192e7a..203e813 100644 --- a/plone/app/discussion/browser/comments.pt +++ b/plone/app/discussion/browser/comments.pt @@ -1,6 +1,7 @@ @@ -42,14 +44,14 @@
- - +
+ +
+ +
+ + + *' + }) /********************************************************************** * 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 ba32492..cc810fa 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 df81bf7..b4bbc35 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -42,6 +42,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 @@ -115,6 +116,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 @@ -193,7 +202,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 ed82463..ee486c3 100644 --- a/plone/app/discussion/configure.zcml +++ b/plone/app/discussion/configure.zcml @@ -43,6 +43,17 @@ for="Products.CMFPlone.interfaces.IPloneSiteRoot" /> + + + diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index 46d430d..cdf662b 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -274,6 +274,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 736da64..0ab4a61 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 1f39098..09bdea2 100644 --- a/plone/app/discussion/tests/test_comment.py +++ b/plone/app/discussion/tests/test_comment.py @@ -130,6 +130,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 9c3fb22..7ed9da4 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): @@ -125,6 +127,65 @@ class TestCommentForm(unittest.TestCase): self.assertEqual(len(errors), 0) self.assertFalse(commentForm.handleComment(commentForm, "foo")) + 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.discussionTool.overrideDiscussionFor(self.portal.doc1, True) @@ -459,9 +520,11 @@ class TestCommentsViewlet(unittest.TestCase): ) def test_get_commenter_portrait_is_none(self): - self.assertEqual( - self.viewlet.get_commenter_portrait(), - 'defaultUser.gif' + 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 a184bb7..ca56c0b 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(