diff --git a/CHANGES.rst b/CHANGES.rst index 6790b6e..55ef5bf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,13 @@ Changelog - Update Traditional Chinese translations. [marr] +- Make comments editable. + [pjstevns, gyst] + +- Provide 'Delete comments' permission to handle comments deletion + [cekk] + + 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..0348f33 100644 --- a/plone/app/discussion/browser/comments.pt +++ b/plone/app/discussion/browser/comments.pt @@ -1,6 +1,7 @@ @@ -87,7 +90,7 @@ action="" method="post" class="commentactionsform" - tal:condition="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 c44a1b1..822ca2b 100644 --- a/plone/app/discussion/browser/controlpanel.py +++ b/plone/app/discussion/browser/controlpanel.py @@ -24,6 +24,7 @@ from z3c.form import button from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget from plone.app.discussion.interfaces import IDiscussionSettings, _ +from plone.app.discussion.upgrades import update_registry class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): @@ -51,6 +52,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): SingleCheckBoxFieldWidget self.fields['moderation_enabled'].widgetFactory = \ SingleCheckBoxFieldWidget + self.fields['edit_comment_enabled'].widgetFactory = \ + SingleCheckBoxFieldWidget self.fields['anonymous_comments'].widgetFactory = \ SingleCheckBoxFieldWidget self.fields['show_commenter_image'].widgetFactory = \ @@ -61,7 +64,13 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): SingleCheckBoxFieldWidget def updateWidgets(self): - super(DiscussionSettingsEditForm, self).updateWidgets() + 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() self.widgets['globally_enabled'].label = _(u"Enable Comments") self.widgets['anonymous_comments'].label = _(u"Anonymous Comments") self.widgets['show_commenter_image'].label = _(u"Commenter Image") @@ -118,6 +127,9 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper): elif settings.moderation_enabled: output.append("moderation_enabled") + if settings.edit_comment_enabled: + output.append("edit_comment_enabled") + # Anonymous comments if settings.anonymous_comments: output.append("anonymous_comments") diff --git a/plone/app/discussion/browser/javascripts/comments.js b/plone/app/discussion/browser/javascripts/comments.js index 4da5af5..81a8d64 100644 --- a/plone/app/discussion/browser/javascripts/comments.js +++ b/plone/app/discussion/browser/javascripts/comments.js @@ -165,6 +165,15 @@ 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. 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/moderation.py b/plone/app/discussion/browser/moderation.py index 55110e8..84748f7 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,28 @@ 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'. + """ + return getSecurityManager().checkPermission('Delete comments', + aq_inner(reply)) + class PublishComment(BrowserView): """Publish a comment. 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..66c5296 100644 --- a/plone/app/discussion/configure.zcml +++ b/plone/app/discussion/configure.zcml @@ -44,6 +44,27 @@ 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..97c6782 100644 --- a/plone/app/discussion/permissions.zcml +++ b/plone/app/discussion/permissions.zcml @@ -9,4 +9,14 @@ title="Review comments" /> + + + + diff --git a/plone/app/discussion/profiles/default/metadata.xml b/plone/app/discussion/profiles/default/metadata.xml index ffedb11..ce1f445 100644 --- a/plone/app/discussion/profiles/default/metadata.xml +++ b/plone/app/discussion/profiles/default/metadata.xml @@ -1,5 +1,5 @@ - 100 + 102 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..83a67fc 100644 --- a/plone/app/discussion/profiles/default/rolemap.xml +++ b/plone/app/discussion/profiles/default/rolemap.xml @@ -3,7 +3,18 @@ - + + + + + + + + + + + + diff --git a/plone/app/discussion/testing.py b/plone/app/discussion/testing.py index e7a397d..90f08f9 100644 --- a/plone/app/discussion/testing.py +++ b/plone/app/discussion/testing.py @@ -34,6 +34,8 @@ class PloneAppDiscussion(PloneSandboxLayer): USER_WITH_FULLNAME_PASSWORD = 'secret' MANAGER_USER_NAME = 'manager' MANAGER_USER_PASSWORD = 'secret' + REVIEWER_NAME = 'reviewer' + REVIEWER_PASSWORD = 'secret' def setUpZope(self, app, configurationContext): # Load ZCML @@ -66,7 +68,15 @@ class PloneAppDiscussion(PloneSandboxLayer): ['Member'], [], ) + acl_users.userFolderAddUser( + self.REVIEWER_NAME, + self.REVIEWER_PASSWORD, + ['Member'], + [], + ) mtool = getToolByName(portal, 'portal_membership', None) + gtool = getToolByName(portal, 'portal_groups', None) + gtool.addPrincipalToGroup(self.REVIEWER_NAME, 'Reviewers') mtool.addMember('jim', 'Jim', ['Member'], []) mtool.getMemberById('jim').setMemberProperties( {"fullname": 'Jim Fult\xc3\xb8rn'}) diff --git a/plone/app/discussion/tests/functional_test_comments.txt b/plone/app/discussion/tests/functional_test_comments.txt index 55a489d..c0d1159 100644 --- a/plone/app/discussion/tests/functional_test_comments.txt +++ b/plone/app/discussion/tests/functional_test_comments.txt @@ -32,6 +32,7 @@ you're not logged in:: >>> unprivileged_browser = Browser(app) >>> browser_member = 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: @@ -143,7 +144,6 @@ Post a comment as member ------------------------ Login as user 'jim'. - >>> browser_member.open(portal_url + '/login_form') >>> browser_member.getControl(name='__ac_name').value = 'jim' >>> browser_member.getControl(name='__ac_password').value = 'secret' @@ -250,6 +250,173 @@ 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 + + +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 --------------------------------------------------- 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..0aff711 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,135 @@ 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.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): self.portal.doc1.allow_discussion = True @@ -477,9 +608,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( diff --git a/plone/app/discussion/upgrades.py b/plone/app/discussion/upgrades.py index bf9df71..652e060 100644 --- a/plone/app/discussion/upgrades.py +++ b/plone/app/discussion/upgrades.py @@ -2,7 +2,13 @@ from zope.component import getUtility from plone.registry.interfaces import IRegistry from plone.app.discussion.interfaces import IDiscussionSettings +default_profile = 'profile-plone.app.discussion:default' + def update_registry(context): registry = getUtility(IRegistry) registry.registerInterface(IDiscussionSettings) + + +def update_rolemap(context): + context.runImportStepFromProfile(default_profile, 'rolemap')