From ee6b107a311f2c221b0680caaa348b1c41c7cbdb Mon Sep 17 00:00:00 2001 From: Katja Suess Date: Mon, 30 Sep 2019 10:34:49 +0200 Subject: [PATCH 01/25] publish only pending comment, else show status message --- plone/app/discussion/browser/moderation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py index e05352e..52c85a2 100644 --- a/plone/app/discussion/browser/moderation.py +++ b/plone/app/discussion/browser/moderation.py @@ -8,6 +8,7 @@ from plone.app.discussion.events import CommentDeletedEvent from plone.app.discussion.interfaces import _ from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IReplies +from plone.protect.interfaces import IDisableCSRFProtection from Products.CMFCore.utils import getToolByName from Products.Five.browser import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile @@ -206,6 +207,7 @@ class PublishComment(BrowserView): """ def __call__(self): + alsoProvides(self.request, IDisableCSRFProtection) comment = aq_inner(self.context) content_object = aq_parent(aq_parent(comment)) workflowTool = getToolByName(comment, 'portal_workflow', None) From 840bc8daa639564d9a26570bef22dc2133001e21 Mon Sep 17 00:00:00 2001 From: Katja Suess Date: Mon, 30 Sep 2019 10:39:24 +0200 Subject: [PATCH 02/25] changelog --- news/163.enhancement | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/163.enhancement diff --git a/news/163.enhancement b/news/163.enhancement new file mode 100644 index 0000000..f6ee230 --- /dev/null +++ b/news/163.enhancement @@ -0,0 +1,2 @@ +link of notification mail: /@@moderate-publish-comment : publish only pending comment, else show status message "comment already approved" +[ksuess] From 875409daffa1cab1b08b9cd09e2b0f141882905b Mon Sep 17 00:00:00 2001 From: Katja Suess Date: Mon, 30 Sep 2019 14:31:59 +0200 Subject: [PATCH 03/25] Moderation view tabbed two tabs for comments to moderate and comments approved --- .../discussion/browser/comments_approved.pt | 129 ------- plone/app/discussion/browser/configure.zcml | 17 - plone/app/discussion/browser/moderation.pt | 322 ++++++++++++------ plone/app/discussion/browser/moderation.py | 29 +- 4 files changed, 214 insertions(+), 283 deletions(-) delete mode 100644 plone/app/discussion/browser/comments_approved.pt diff --git a/plone/app/discussion/browser/comments_approved.pt b/plone/app/discussion/browser/comments_approved.pt deleted file mode 100644 index 1149e97..0000000 --- a/plone/app/discussion/browser/comments_approved.pt +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - -

- Comments approved -

- -
- Warning - - Moderation workflow is disabled. You have to - - enable the 'Comment Review Workflow' for the Comment content - type before you can moderate comments here. - -
- -
-
-

- No comments approved -

-
-
- -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
CommenterDateIn Response ToCommentApproved byAction
- Name - -
- Email - -
-
- - - - - show full comment text - - - last history entry - - - - -
-
-
-
- -
-
- - - diff --git a/plone/app/discussion/browser/configure.zcml b/plone/app/discussion/browser/configure.zcml index 6caff55..7bb5aac 100644 --- a/plone/app/discussion/browser/configure.zcml +++ b/plone/app/discussion/browser/configure.zcml @@ -29,23 +29,6 @@ permission="plone.app.discussion.ReviewComments" /> - - - - - Moderate comments -

- > Comments approved -

-
-
-

- No comments to moderate. -

+
+
+ + Moderate comments +
+ +
+

+ No comments to moderate. +

+
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
CommenterDateIn Response ToCommentAction
+ + + + Name + +
+ Email + +
+
+ + + + + show full comment text + + + +
+
+
+
+
- +
+ + Approved comments +
+
+
+

+ No comments approved +

+
+
-
+ -
+
-
+
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
CommenterDateIn Response ToCommentAction
- - - - Name - -
- Email - -
-
- - - - - show full comment text - - - -
-
+ + + + + + + + + + + + + + + + + + + + + + + +
CommenterDateIn Response ToCommentApproved byAction
+ Name + +
+ Email + +
+
+ + + + + show full comment text + + + last history entry + + + + +
+
+
+ + +
- +
+ + diff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py index 52c85a2..ac9af20 100644 --- a/plone/app/discussion/browser/moderation.py +++ b/plone/app/discussion/browser/moderation.py @@ -35,6 +35,10 @@ class View(BrowserView): review_state='pending', sort_on='created', sort_order='reverse') + self.comments_approved = catalog(object_provides=IComment.__identifier__, + review_state='published', + sort_on='created', + sort_order='reverse') return self.template() def moderation_enabled(self): @@ -54,31 +58,6 @@ class View(BrowserView): return False -class ApprovedView(View): - """Overview comments already approved.""" - template = ViewPageTemplateFile('comments_approved.pt') - try: - template.id = '@@comments-approved' - except AttributeError: - # id is not writeable in Zope 2.12 - pass - - def __call__(self): - self.request.set('disable_border', True) - context = aq_inner(self.context) - catalog = getToolByName(context, 'portal_catalog') - self.comments = catalog(object_provides=IComment.__identifier__, - review_state='published', - sort_on='created', - sort_order='reverse') - - # print("*** approved comments") - # print(self.comments) - # for el in self.comments: - # print(el.id, el.review_state) - return self.template() - - class ModerateCommentsEnabled(BrowserView): def __call__(self): From f7b8335d272f5587237866eb53f10dd50be1240f Mon Sep 17 00:00:00 2001 From: Katja Suess Date: Mon, 30 Sep 2019 16:43:19 +0200 Subject: [PATCH 04/25] additional workflow with rejected state moderation view and approved comments view: buttons for reject and approve --- plone/app/discussion/browser/configure.zcml | 9 ++ .../browser/javascripts/moderation.js | 57 ++++++++-- plone/app/discussion/browser/moderation.pt | 40 +++++-- plone/app/discussion/browser/moderation.py | 57 +++++++++- .../discussion/profiles/default/workflows.xml | 1 + .../definition.xml | 105 ++++++++++++++++++ 6 files changed, 252 insertions(+), 17 deletions(-) create mode 100644 plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml diff --git a/plone/app/discussion/browser/configure.zcml b/plone/app/discussion/browser/configure.zcml index 7bb5aac..c089074 100644 --- a/plone/app/discussion/browser/configure.zcml +++ b/plone/app/discussion/browser/configure.zcml @@ -100,6 +100,15 @@ permission="plone.app.discussion.ReviewComments" /> + + + tbody > tr"); - if (comments.length === 1) { + if (moderate) { + // fade out row + $(row).fadeOut("normal", function () { + $(this).remove(); + }); + // reload page if all comments have been removed + var comments = $("table#review-comments > tbody > tr"); + if (comments.length === 1) { + location.reload(); + } + } else { + location.reload(); + } + }, + error: function (msg) { // jshint ignore:line + alert("Error sending AJAX request:" + target); + } + }); + }); + + + /********************************************************************** + * Reject a single comment. + **********************************************************************/ + $("input[name='form.button.Reject']").click(function (e) { + e.preventDefault(); + var row = $(this).parent().parent(); + var path = $(row).find("[name='selected_obj_paths:list']").attr("value"); + var auth_key = $('input[name="_authenticator"]').val(); + var target = path + "/@@moderate-reject-comment?_authenticator=" + auth_key; + var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments"; + $.ajax({ + type: "GET", + url: target, + success: function (msg) { // jshint ignore:line + if (moderate) { + // fade out row + $(row).fadeOut("normal", function () { + $(this).remove(); + }); + // reload page if all comments have been removed + var comments = $("table#review-comments > tbody > tr"); + if (comments.length === 1) { + location.reload(); + } + } else { location.reload(); } }, @@ -171,7 +211,8 @@ require([ // jshint ignore:line **********************************************************************/ $(".last-history-entry").each(function() { $(this).load($(this).attr("data-href") + " .historyByLine", function() { - $(this).children(".historyByLine").last().remove(); + let currententry = $(this).children(".historyByLine").first(); + $(this).html(currententry); }); }); diff --git a/plone/app/discussion/browser/moderation.pt b/plone/app/discussion/browser/moderation.pt index 6f5aea0..e3e56bb 100644 --- a/plone/app/discussion/browser/moderation.pt +++ b/plone/app/discussion/browser/moderation.pt @@ -142,7 +142,16 @@ name="form.button.Publish" i18n:attributes="value label_publish;" tal:attributes="id item/id" - tal:condition="python:item.review_state == 'pending'" + tal:condition="python:item.review_state in ['pending',]" + /> +
-
+

No comments approved

@@ -179,7 +188,7 @@ tal:condition="items_approved_or_rejected" tal:define="batch python:Batch(items_approved_or_rejected, b_size, int(b_start), orphan=1);"> -
+
@@ -190,7 +199,7 @@ Date In Response To Comment - Approved by + Last Action Action @@ -215,8 +224,8 @@ - - + + - + + + diff --git a/plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml b/plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml new file mode 100644 index 0000000..14c53cc --- /dev/null +++ b/plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml @@ -0,0 +1,105 @@ + + + Access contents information + Modify portal content + Reply to item + View + + Submitted, pending review. + + + + Manager + Owner + Reviewer + + + Manager + Owner + Reviewer + + + + + Manager + Owner + Reviewer + + + + Visible to everyone, non-editable. + + + + + Manager + + + + + + + + + + + Approving the comment makes it visible to other users. + Approve + + Review comments + + + + Reject + + Review comments + + + + Previous transition + + + transition/getId|nothing + + + + + + The ID of the user who performed the previous transition + + + user/getUserName + + + + + + Comment about the last transition + + + python:state_change.kwargs.get('comment', '') + + + + + + Provides access to workflow history + + + state_change/getHistory + + + Request review + Review portal content + + + + When the previous transition was performed + + + state_change/getDateTime + + + + + From 084d2893e7222a984c31112283ddd9e671981fba Mon Sep 17 00:00:00 2001 From: Katja Suess Date: Thu, 5 Dec 2019 21:55:23 +0100 Subject: [PATCH 05/25] Additional (optional) workflow: "Comment Multiple State Review Workflow" Moderator is not forced to delete a comment or to let it pending: Workflow has two more states "rejected" and "spam" to existing review workflow. Moderation view extended showing all states. Filter by state. --- news/164.enhancement | 3 + plone/app/discussion/browser/comments.pt | 11 +- plone/app/discussion/browser/configure.zcml | 14 +- plone/app/discussion/browser/controlpanel.py | 9 +- .../browser/javascripts/comments.js | 8 +- .../browser/javascripts/moderation.js | 427 +++++++++--------- plone/app/discussion/browser/moderation.pt | 390 +++++++--------- plone/app/discussion/browser/moderation.py | 161 +++++-- .../discussion/profiles/default/metadata.xml | 2 +- .../discussion/profiles/default/workflows.xml | 2 +- .../definition.xml | 138 ++++++ ...unctional_test_comment_review_workflow.txt | 6 +- .../tests/functional_test_comments.txt | 6 +- .../discussion/tests/test_moderation_view.py | 6 +- plone/app/discussion/tests/test_workflow.py | 4 +- plone/app/discussion/upgrades.py | 20 +- plone/app/discussion/upgrades.zcml | 11 + 17 files changed, 684 insertions(+), 534 deletions(-) create mode 100644 news/164.enhancement create mode 100644 plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml diff --git a/news/164.enhancement b/news/164.enhancement new file mode 100644 index 0000000..6feb63a --- /dev/null +++ b/news/164.enhancement @@ -0,0 +1,3 @@ +Additional optional workflow: workflow with two more states: rejected and spam, + added to existing states pending and published. Moderation view extended to handle four workflow states. +[ksuess] diff --git a/plone/app/discussion/browser/comments.pt b/plone/app/discussion/browser/comments.pt index bf6671b..1dd26b6 100644 --- a/plone/app/discussion/browser/comments.pt +++ b/plone/app/discussion/browser/comments.pt @@ -39,8 +39,9 @@ portrait_url python:view.get_commenter_portrait(reply.author_username); 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); + canDelete python:view.can_delete(reply); + colorclass python:lambda x: 'state-private' if x=='rejected' else ('state-internal' if x=='spam' else 'state-'+x);" + tal:attributes="class python:'comment replyTreeLevel{depth} {state}'.format(depth= depth, state=colorclass(review_state)); id string:${reply/getId}" tal:condition="python:canReview or review_state == 'published'"> @@ -119,7 +120,7 @@ - Edit @@ -146,10 +147,10 @@ class="commentactionsform" tal:condition="canReview" tal:repeat="action reply_dict/actions|nothing" - tal:attributes="action string:${reply/absolute_url}/@@moderate-publish-comment; + tal:attributes="action string:${reply/absolute_url}/@@transmit-comment; name action/id"> - - + - - tbody > tr"); - if (comments.length === 1) { - location.reload(); - } - }, - error: function (msg) { // jshint ignore:line - alert("Error sending AJAX request:" + target); - } - }); - }); - - - /********************************************************************** - * Publish a single comment. - **********************************************************************/ - $("input[name='form.button.Publish']").click(function (e) { - e.preventDefault(); - var row = $(this).parent().parent(); - var path = $(row).find("[name='selected_obj_paths:list']").attr("value"); - var auth_key = $('input[name="_authenticator"]').val(); - var target = path + "/@@moderate-publish-comment?_authenticator=" + auth_key; - var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments"; - $.ajax({ - type: "GET", - url: target, - success: function (msg) { // jshint ignore:line - if (moderate) { - // fade out row - $(row).fadeOut("normal", function () { - $(this).remove(); - }); - // reload page if all comments have been removed - var comments = $("table#review-comments > tbody > tr"); - if (comments.length === 1) { - location.reload(); - } - } else { - location.reload(); - } - }, - error: function (msg) { // jshint ignore:line - alert("Error sending AJAX request:" + target); - } - }); - }); - - - /********************************************************************** - * Reject a single comment. - **********************************************************************/ - $("input[name='form.button.Reject']").click(function (e) { - e.preventDefault(); - var row = $(this).parent().parent(); - var path = $(row).find("[name='selected_obj_paths:list']").attr("value"); - var auth_key = $('input[name="_authenticator"]').val(); - var target = path + "/@@moderate-reject-comment?_authenticator=" + auth_key; - var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments"; - $.ajax({ - type: "GET", - url: target, - success: function (msg) { // jshint ignore:line - if (moderate) { - // fade out row - $(row).fadeOut("normal", function () { - $(this).remove(); - }); - // reload page if all comments have been removed - var comments = $("table#review-comments > tbody > tr"); - if (comments.length === 1) { - location.reload(); - } - } else { - location.reload(); - } - }, - error: function (msg) { // jshint ignore:line - alert("Error sending AJAX request:" + target); - } - }); - }); - - - /********************************************************************** - * Bulk actions for comments (delete, publish) - **********************************************************************/ - $("input[name='form.button.BulkAction']").click(function (e) { - e.preventDefault(); - var form = $(this).parents("form"); - var target = $(form).attr('action'); - var params = $(form).serialize(); - var valArray = $('input:checkbox:checked'); - var selectField = $(form).find("[name='form.select.BulkAction']"); - if (selectField.val() === '-1') { - // XXX: translate message - alert("You haven't selected a bulk action. Please select one."); - } else if (valArray.length === 0) { - // XXX: translate message - alert("You haven't selected any comment for this bulk action." + - "Please select at least one comment."); - } else { - $.post(target, params, function (data) { // jshint ignore:line - valArray.each(function () { - /* Remove all selected lines. */ - var row = $(this).parent().parent(); - row.fadeOut("normal", function () { - row.remove(); - }); - }); - // reload page if all comments have been removed - var comments = $("table#review-comments > tbody > tr"); - if (comments.length <= valArray.length) { - location.reload(); - } - }); - // reset the bulkaction select - selectField.find("option[value='-1']").attr('selected', 'selected'); - } - }); - - - /********************************************************************** - * Check or uncheck all checkboxes from the batch moderation page. - **********************************************************************/ - $("input[name='check_all']").click(function () { - if ($(this).val() === '0') { - $(this).parents("table") - .find("input:checkbox") - .attr("checked", "checked"); - $(this).val("1"); - } else { - $(this).parents("table") - .find("input:checkbox") - .attr("checked", ""); - $(this).val("0"); - } - }); - - - /********************************************************************** - * Show full text of a comment in the batch moderation page. - **********************************************************************/ - $(".show-full-comment-text").click(function (e) { - e.preventDefault(); - var target = $(this).attr("href"); - var td = $(this).parent(); - $.ajax({ - type: "GET", - url: target, - data: "", - success: function (data) { - // show full text - td.replaceWith("" + data + ""); - }, - error: function (msg) { // jshint ignore:line - alert("Error getting full comment text:" + target); - } - }); - }); - - - /********************************************************************** - * Comments approved: Load history for approved date. - **********************************************************************/ - $(".last-history-entry").each(function() { - $(this).load($(this).attr("data-href") + " .historyByLine", function() { - let currententry = $(this).children(".historyByLine").first(); - $(this).html(currententry); - }); - }); + $(document).ready(function() { + init(); + }); + function init() { + /********************************************************************** + * Delete a single comment. + **********************************************************************/ + $("input[name='form.button.moderation.DeleteComment']").click(function(e) { + e.preventDefault(); + var row = $(this).closest("tr"); + var path = row.find("[name='selected_obj_paths:list']").attr("value"); + var auth_key = $('input[name="_authenticator"]').val(); + var target = + path + "/@@moderate-delete-comment?_authenticator=" + auth_key; + $.ajax({ + type: "GET", + url: target, + success: function(msg) { + // fade out row + row.fadeOut(250).fadeIn(250, function() { + row.remove(); + }); + // reload page if all comments have been removed + var comments = $("table#review-comments > tbody > tr"); + if (comments.length === 1) { + location.reload(); + } + }, + error: function(msg) { + alert("Error sending AJAX request:" + target); + } + }); }); - //#JSCOVERAGE_ENDIF + /********************************************************************** + * Transmit a single comment. + **********************************************************************/ + $('input[name="form.button.moderation.TransmitComment"]').click(function( + e + ) { + e.preventDefault(); + let button = $(this); + var row = $(this).closest("tr"); + var path = $(row) + .find("[name='selected_obj_paths:list']") + .attr("value"); + var workflow_action = $(this).attr("data-transition"); + var auth_key = $('input[name="_authenticator"]').val(); + // distinction of workflow_action + var target = + path + + "/@@transmit-comment?_authenticator=" + + auth_key + + "&workflow_action=" + + workflow_action; + var moderate = + $(this) + .closest("fieldset") + .attr("id") == "fieldset-moderate-comments"; + $.ajax({ + type: "GET", + url: target, + success: function(msg) { + if (moderate) { + let url = location.href; + $("#review-comments").load(url + " #review-comments", function() { + init(); + $(".pat-plone-modal").patPloneModal(); + }); + } else { + location.reload(); + } + }, + error: function(msg) { + alert( + "Error transmitting comment. (Error sending AJAX request:" + + target + + ")" + ); + } + }); + }); + /********************************************************************** + * Bulk actions for comments (delete, publish) + **********************************************************************/ + $("input[name='form.button.BulkAction']").click(function(e) { + e.preventDefault(); + var form = $(this).parents("form"); + var target = $(form).attr("action"); + var params = $(form).serialize(); + var valArray = $("input:checkbox:checked"); + var selectField = $(form).find("[name='form.select.BulkAction']"); + if (selectField.val() === "-1") { + // XXX: translate message + alert("You haven't selected a bulk action. Please select one."); + } else if (valArray.length === 0) { + // XXX: translate message + alert( + "You haven't selected any comment for this bulk action." + + "Please select at least one comment." + ); + } else { + $.post(target, params, function(data) { + valArray.each(function() { + /* Remove all selected lines. */ + var row = $(this) + .parent() + .parent(); + row.fadeOut("normal", function() { + row.remove(); + }); + }); + // reload page if all comments have been removed + var comments = $("table#review-comments > tbody > tr"); + if (comments.length <= valArray.length) { + location.reload(); + } + }); + // reset the bulkaction select + selectField.find("option[value='-1']").attr("selected", "selected"); + } + }); + + /********************************************************************** + * Check or uncheck all checkboxes from the batch moderation page. + **********************************************************************/ + $("input[name='check_all']").click(function() { + if ($(this).val() === "0") { + $(this) + .parents("table") + .find("input:checkbox") + .prop("checked", true); + $(this).val("1"); + } else { + $(this) + .parents("table") + .find("input:checkbox") + .prop("checked", false); + $(this).val("0"); + } + }); + + /********************************************************************** + * select comments with review_state + **********************************************************************/ + + $("input[name='review_state']").click(function() { + // location.search = 'review_state=' + $(this).val(); + let review_state = $(this).val(); + let url = location.href; + if (location.search) { + url = location.href.replace( + location.search, + "?review_state=" + review_state + ); + } else { + url = location.href + "?review_state=" + review_state; + } + + $("#review-comments").load(url + " #review-comments", function() { + init(); + $(".pat-plone-modal").patPloneModal(); + let stateObj = { review_state: review_state }; + history.pushState(stateObj, "moderate comments", url); + }); + }); + + /********************************************************************** + * Show full text of a comment in the batch moderation page. + **********************************************************************/ + $(".show-full-comment-text").click(function(e) { + e.preventDefault(); + var target = $(this).attr("href"); + var parent = $(this).parent(); + $.ajax({ + type: "GET", + url: target, + data: "", + success: function(data) { + // show full text + parent.html(data); + }, + error: function(msg) { + alert("Error getting full comment text:" + target); + } + }); + }); + + /********************************************************************** + * Comments approved: Load history for approved date. + **********************************************************************/ + $(".last-history-entry").each(function() { + var me = $(this); + $.ajax({ + url: me.attr("data-href"), + success: function(data) { + let first_history_entry = $(data) + .find(".historyByLine") + .first(); + me.html(""); + first_history_entry.children().each(function() { + me.append($(this)); + me.append("
"); + }); + // format date + registry.scan(me); + }, + error: function(msg) { + alert("Error getting history."); + } + }); + }); + } // end init }); diff --git a/plone/app/discussion/browser/moderation.pt b/plone/app/discussion/browser/moderation.pt index e3e56bb..fb99297 100644 --- a/plone/app/discussion/browser/moderation.pt +++ b/plone/app/discussion/browser/moderation.pt @@ -17,7 +17,9 @@ b_size python:30; b_start python:0; b_start request/b_start | b_start; - moderation_enabled view/moderation_enabled;"> + moderation_enabled view/moderation_enabled; + colorclass python:lambda x: 'state-private' if x=='rejected' else ('state-internal' if x=='spam' else 'state-'+x); + "> @@ -40,245 +42,163 @@ -
-
- - Moderate comments -
- -
-

- No comments to moderate. -

-
- -
+ +
+

+ No comments +

+
+
-
+
-
+
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
CommenterDateIn Response ToCommentAction
- - - - Name - -
- Email - -
-
- - - - - show full comment text - - - - -
-
-
- -
-
-
- - Approved comments -
-
-
-

- No comments approved -

-
-
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+ + + +
CommenterDateIn Response ToCommentLast Action
+ + + + Name + +
+ Email + +
+
+ + + + +
+ + + + + Edit -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - -
CommenterDateIn Response ToCommentLast ActionAction
- Name - -
- Email - -
-
- - - - - show full comment text - - - last history entry - - - - - - -
-
-
-
- -
- - + + + + + +
+ + last history entry + +
+
+
+ diff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py index 977b08a..2ed64d7 100644 --- a/plone/app/discussion/browser/moderation.py +++ b/plone/app/discussion/browser/moderation.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 from AccessControl import getSecurityManager from AccessControl import Unauthorized from Acquisition import aq_inner @@ -17,9 +17,23 @@ from zope.event import notify from zope.interface import alsoProvides +# Translations for generated values in buttons +# States +_('comment_pending', default='pending') +# _('comment_approved', default='approved') +_('comment_published', default='approved') +_('comment_rejected', default='rejected') +_('comment_spam', default='marked as spam') +# Transitions +_('Recall') +_('Approve') +_('Reject') +_('Spam') + + class View(BrowserView): - """Comment moderation view. - """ + """Show comment moderation view.""" + template = ViewPageTemplateFile('moderation.pt') try: template.id = '@@moderate-comments' @@ -27,53 +41,91 @@ class View(BrowserView): # id is not writeable in Zope 2.12 pass + def __init__(self, context, request): + self.context = context + self.request = request + self.workflowTool = getToolByName(self.context, 'portal_workflow') + def __call__(self): self.request.set('disable_border', True) - context = aq_inner(self.context) - catalog = getToolByName(context, 'portal_catalog') - self.comments = catalog(object_provides=IComment.__identifier__, - review_state='pending', - sort_on='created', - sort_order='reverse') - self.comments_approved = catalog(object_provides=IComment.__identifier__, - review_state=['published', 'rejected'], - sort_on='created', - sort_order='reverse') + self.request.set('review_state', + self.request.get('review_state', 'pending')) return self.template() - def moderation_enabled(self): - """Returns true if a 'review workflow' is enabled on 'Discussion Item' - content type. A 'review workflow' is characterized by implementing - a 'pending' workflow state. + def comments(self): + """Return comments of defined review_state. + + review_state is string or list of strings. """ - context = aq_inner(self.context) - workflowTool = getToolByName(context, 'portal_workflow') - comment_workflow = workflowTool.getChainForPortalType( + catalog = getToolByName(self.context, 'portal_catalog') + if self.request.review_state == 'all': + return catalog(object_provides=IComment.__identifier__, + sort_on='created', + sort_order='reverse') + return catalog(object_provides=IComment.__identifier__, + review_state=self.request.review_state, + sort_on='created', + sort_order='reverse') + + def moderation_enabled(self): + """Return true if a review workflow is enabled on 'Discussion Item' + content type. + + A 'review workflow' is characterized by implementing a 'pending' + workflow state. + """ + comment_workflow = self.workflowTool.getChainForPortalType( 'Discussion Item') if comment_workflow: comment_workflow = comment_workflow[0] - comment_workflow = workflowTool[comment_workflow] + comment_workflow = self.workflowTool[comment_workflow] if 'pending' in comment_workflow.states: return True return False @property - def moderation_3state(self): - """Returns true if a 'review 3 state workflow' is enabled on 'Discussion Item' - content type. A 'review 3 state workflow' is characterized by implementing - a 'rejected' workflow state. + def moderation_multiple_state_enabled(self): + """Return true if a 'review multiple state workflow' is enabled on + 'Discussion Item' content type. + + A 'review multipe state workflow' is characterized by implementing + a 'rejected' workflow state and a 'spam' workflow state. """ - context = aq_inner(self.context) - workflowTool = getToolByName(context, 'portal_workflow') - comment_workflow = workflowTool.getChainForPortalType( + comment_workflow = self.workflowTool.getChainForPortalType( 'Discussion Item') if comment_workflow: comment_workflow = comment_workflow[0] - comment_workflow = workflowTool[comment_workflow] + comment_workflow = self.workflowTool[comment_workflow] if 'rejected' in comment_workflow.states: return True return False + def allowed_transitions(self, obj=None): + """Return allowed workflow transitions. + + Example: pending + + [{'id': 'mark_as_spam', 'url': 'http://localhost:8083/PloneRejected/testfolder/testpage/++conversation++default/1575415863542780/content_status_modify?workflow_action=mark_as_spam', 'icon': '', 'category': 'workflow', 'transition': , 'title': 'Spam', 'link_target': None, 'visible': True, 'available': True, 'allowed': True}, + {'id': 'publish', + 'url': 'http://localhost:8083/PloneRejected/testfolder/testpage/++conversation++default/1575415863542780/content_status_modify?workflow_action=publish', + 'icon': '', + 'category': 'workflow', + 'transition': , + 'title': 'Approve', + 'link_target': None, 'visible': True, 'available': True, 'allowed': True}, + {'id': 'reject', 'url': 'http://localhost:8083/PloneRejected/testfolder/testpage/++conversation++default/1575415863542780/content_status_modify?workflow_action=reject', 'icon': '', 'category': 'workflow', 'transition': , 'title': 'Reject', 'link_target': None, 'visible': True, 'available': True, 'allowed': True}] + """ + + if obj: + transitions = [ + a for a in self.workflowTool.listActionInfos(object=obj) + if a['category'] == 'workflow' and a['allowed'] + ] + return transitions + + def translate(self, text=""): + return _(text) + class ModerateCommentsEnabled(BrowserView): @@ -181,13 +233,13 @@ class DeleteOwnComment(DeleteComment): raise Unauthorized("You're not allowed to delete this comment.") -class PublishComment(BrowserView): - """Publish a comment. +class CommentTransition(BrowserView): + r"""Publish, reject, recall a comment or mark it as spam. This view is always called directly on the comment object: http://nohost/front-page/++conversation++default/1286289644723317/\ - @@moderate-publish-comment + @@transmit-comment Each table row (comment) in the moderation view contains a hidden input field with the absolute URL of the content object: @@ -203,31 +255,39 @@ class PublishComment(BrowserView): """ def __call__(self): - alsoProvides(self.request, IDisableCSRFProtection) + """Call CommentTransition.""" comment = aq_inner(self.context) content_object = aq_parent(aq_parent(comment)) print("*** called: PublishComment for ", comment.Description) workflowTool = getToolByName(comment, 'portal_workflow', None) workflow_action = self.request.form.get('workflow_action', 'publish') review_state = workflowTool.getInfoFor(comment, 'review_state', '') - if review_state != "published": - workflowTool.doActionFor(comment, workflow_action) - comment.reindexObject() - content_object.reindexObject(idxs=['total_comments']) - notify(CommentPublishedEvent(self.context, comment)) - IStatusMessage(self.context.REQUEST).addStatusMessage( - _('Comment approved.'), - type='info') - else: - IStatusMessage(self.context.REQUEST).addStatusMessage( - _('Comment already approved.'), - type='info') + workflowTool.doActionFor(comment, workflow_action) + comment.reindexObject() + content_object.reindexObject(idxs=['total_comments']) + notify(CommentPublishedEvent(self.context, comment)) + review_state_new = workflowTool.getInfoFor(comment, 'review_state', '') + + # context.translate() does not know a default for untranslated msgids + comment_state_translated = \ + self.context.translate("comment_"+review_state_new) + if comment_state_translated == "comment_"+review_state_new: + comment_state_translated = review_state_new + + msgid = _( + "comment_transmitted", + default='Comment ${comment_state_translated}.', + mapping={"comment_state_translated": comment_state_translated}) + translated = self.context.translate(msgid) + IStatusMessage(self.context.REQUEST).addStatusMessage( + translated, 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 or - not getToolByName( - content_object, 'portal_url').isURLInPortal(came_from) or - '@@confirm-action' in came_from): + if (len(came_from) == 0 + or 'came_from=' in came_from + or not getToolByName( + content_object, 'portal_url').isURLInPortal(came_from)): came_from = content_object.absolute_url() return self.context.REQUEST.RESPONSE.redirect(came_from) @@ -268,7 +328,7 @@ class RejectComment(BrowserView): class BulkActionsView(BrowserView): - """Bulk actions (unapprove, approve, delete, mark as spam). + """Bulk actions (approve, delete, reject, recall, mark as spam). Each table row of the moderation view has a checkbox with the absolute path (without host and port) of the comment objects: @@ -291,6 +351,7 @@ class BulkActionsView(BrowserView): """ def __call__(self): + """Call BulkActionsView.""" if 'form.select.BulkAction' in self.request: bulkaction = self.request.get('form.select.BulkAction') self.paths = self.request.get('paths') diff --git a/plone/app/discussion/profiles/default/metadata.xml b/plone/app/discussion/profiles/default/metadata.xml index c779e86..49f2d5e 100644 --- a/plone/app/discussion/profiles/default/metadata.xml +++ b/plone/app/discussion/profiles/default/metadata.xml @@ -1,5 +1,5 @@ - 1001 + 1002 profile-plone.resource:default profile-plone.app.registry:default diff --git a/plone/app/discussion/profiles/default/workflows.xml b/plone/app/discussion/profiles/default/workflows.xml index 536fb37..820f22d 100644 --- a/plone/app/discussion/profiles/default/workflows.xml +++ b/plone/app/discussion/profiles/default/workflows.xml @@ -1,7 +1,7 @@ - + diff --git a/plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml b/plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml new file mode 100644 index 0000000..c775960 --- /dev/null +++ b/plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml @@ -0,0 +1,138 @@ + + + Access contents information + Modify portal content + Reply to item + View + + Submitted, pending review. + + + + + Manager + Owner + Reviewer + + + Manager + Owner + Reviewer + + + + + Manager + Owner + Reviewer + + + + Visible to everyone, non-editable. + + + + + + + Manager + + + + + + + + + + + + + + + + + + Spam comments are invisible to other users. + Spam + + Review comments + + + + Approving the comment makes it visible to other users. + Approve + + Review comments + + + + Reject + + Review comments + + + + Rejected comments are invisible to other users. + Reject + + Review comments + + + + Previous transition + + + transition/getId|nothing + + + + + + The ID of the user who performed the previous transition + + + user/getUserName + + + + + + Comment about the last transition + + + python:state_change.kwargs.get('comment', '') + + + + + + Provides access to workflow history + + + state_change/getHistory + + + Request review + Review portal content + + + + When the previous transition was performed + + + state_change/getDateTime + + + + + diff --git a/plone/app/discussion/tests/functional_test_comment_review_workflow.txt b/plone/app/discussion/tests/functional_test_comment_review_workflow.txt index a835f96..5531888 100644 --- a/plone/app/discussion/tests/functional_test_comment_review_workflow.txt +++ b/plone/app/discussion/tests/functional_test_comment_review_workflow.txt @@ -112,7 +112,7 @@ Administrators can see all posts and comment actions >>> 'form.button.DeleteComment' in browser.contents True - >>> 'form.button.PublishComment' in browser.contents + >>> 'form.button.TransmitComment' in browser.contents True Anonymous user can not see any posts or comment actions @@ -128,7 +128,7 @@ Anonymous user can not see any posts or comment actions >>> 'form.button.DeleteComment' in unprivileged_browser.contents False - >>> 'form.button.PublishComment' in unprivileged_browser.contents + >>> 'form.button.TransmitComment' in unprivileged_browser.contents False The catalog does not list the comments yet: @@ -156,7 +156,7 @@ actions. >>> 'form.button.DeleteComment' in browser.contents True - >>> 'form.button.PublishComment' in browser.contents + >>> 'form.button.TransmitComment' in browser.contents True diff --git a/plone/app/discussion/tests/functional_test_comments.txt b/plone/app/discussion/tests/functional_test_comments.txt index 36ad8a1..89bdecc 100644 --- a/plone/app/discussion/tests/functional_test_comments.txt +++ b/plone/app/discussion/tests/functional_test_comments.txt @@ -326,19 +326,19 @@ Deleting existing comments | 'Delete comments' permission Anonymous cannot delete comments >>> unprivileged_browser.open(urldoc1) - >>> 'form.button.Delete' in unprivileged_browser.contents + >>> 'form.button.DeleteComment' 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 + >>> 'form.button.DeleteComment' in browser_member.contents False Admin can delete comments >>> browser.open(urldoc1) - >>> 'form.button.Delete' in browser.contents + >>> 'form.button.DeleteComment' in browser.contents True Extract the delete comment url from the first "delete comment" button diff --git a/plone/app/discussion/tests/test_moderation_view.py b/plone/app/discussion/tests/test_moderation_view.py index 7e660c1..08d6cbd 100644 --- a/plone/app/discussion/tests/test_moderation_view.py +++ b/plone/app/discussion/tests/test_moderation_view.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from plone.app.discussion.browser.moderation import BulkActionsView from plone.app.discussion.browser.moderation import DeleteComment -from plone.app.discussion.browser.moderation import PublishComment +from plone.app.discussion.browser.moderation import CommentTransition from plone.app.discussion.browser.moderation import View from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IDiscussionSettings @@ -195,14 +195,14 @@ class RedirectionTest(unittest.TestCase): def test_regression(self): page_url = self.page.absolute_url() self.request['HTTP_REFERER'] = page_url - for Klass in (DeleteComment, PublishComment): + for Klass in (DeleteComment, CommentTransition): view = Klass(self.comment, self.request) view.__parent__ = self.comment self.assertEqual(page_url, view()) def test_valid_next_url(self): self.request['HTTP_REFERER'] = 'http://attacker.com' - for Klass in (DeleteComment, PublishComment): + for Klass in (DeleteComment, CommentTransition): view = Klass(self.comment, self.request) view.__parent__ = self.comment self.assertNotEqual('http://attacker.com', view()) diff --git a/plone/app/discussion/tests/test_workflow.py b/plone/app/discussion/tests/test_workflow.py index aafe222..9f959fc 100644 --- a/plone/app/discussion/tests/test_workflow.py +++ b/plone/app/discussion/tests/test_workflow.py @@ -272,7 +272,7 @@ class CommentReviewWorkflowTest(unittest.TestCase): 'review_state', ), ) - view = self.comment.restrictedTraverse('@@moderate-publish-comment') + view = self.comment.restrictedTraverse('@@transmit-comment') view() self.assertEqual( 'published', @@ -295,7 +295,7 @@ class CommentReviewWorkflowTest(unittest.TestCase): self.assertRaises( Unauthorized, self.comment.restrictedTraverse, - '@@moderate-publish-comment', + '@@transmit-comment', ) self.assertEqual( 'pending', diff --git a/plone/app/discussion/upgrades.py b/plone/app/discussion/upgrades.py index 7a0e30c..2bb3c28 100644 --- a/plone/app/discussion/upgrades.py +++ b/plone/app/discussion/upgrades.py @@ -20,7 +20,7 @@ def update_rolemap(context): context.runImportStepFromProfile(default_profile, 'rolemap') -def upgrade_comment_workflows(context): +def upgrade_comment_workflows_retain_current_workflow(context): # If the current comment workflow is the one_state_workflow, running our # import step will change it to comment_one_state_workflow. This is good. # If it was anything else, we should restore this. So get the original @@ -46,13 +46,16 @@ def upgrade_comment_workflows(context): orig_chain[idx] = 'comment_one_state_workflow' # Restore the chain. wf_tool.setChainForPortalTypes([portal_type], orig_chain) - new_chain = list(wf_tool.getChainFor(portal_type)) - workflows = [wf_tool.getWorkflowById(wf_id) - for wf_id in new_chain] + +def upgrade_comment_workflows_apply_rolemapping(context): # Now go over the comments, update their role mappings, and reindex the # allowedRolesAndUsers index. + portal_type = 'Discussion Item' catalog = getToolByName(context, 'portal_catalog') + wf_tool = getToolByName(context, 'portal_workflow') + new_chain = list(wf_tool.getChainFor(portal_type)) + workflows = [wf_tool.getWorkflowById(wf_id) for wf_id in new_chain] for brain in catalog.unrestrictedSearchResults(portal_type=portal_type): try: comment = brain.getObject() @@ -63,5 +66,14 @@ def upgrade_comment_workflows(context): logger.info('Could not reindex comment {0}'.format(brain.getURL())) +def upgrade_comment_workflows(context): + upgrade_comment_workflows_retain_current_workflow(context) + upgrade_comment_workflows_apply_rolemapping(context) + + def add_js_to_plone_legacy(context): context.runImportStepFromProfile(default_profile, 'plone.app.registry') + + +def add_multiple_state_workflow(context): + upgrade_comment_workflows_retain_current_workflow(context) diff --git a/plone/app/discussion/upgrades.zcml b/plone/app/discussion/upgrades.zcml index 02a7e54..d64591c 100644 --- a/plone/app/discussion/upgrades.zcml +++ b/plone/app/discussion/upgrades.zcml @@ -62,4 +62,15 @@ /> + + + + From 64d50fbd00b80a5bb5d108b8a63ab9d1b0b47ed2 Mon Sep 17 00:00:00 2001 From: Katja Suess Date: Thu, 5 Dec 2019 23:02:16 +0100 Subject: [PATCH 06/25] fix docstring --- plone/app/discussion/browser/moderation.pt | 1 - plone/app/discussion/browser/moderation.py | 25 +++++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/plone/app/discussion/browser/moderation.pt b/plone/app/discussion/browser/moderation.pt index fb99297..5b8b64c 100644 --- a/plone/app/discussion/browser/moderation.pt +++ b/plone/app/discussion/browser/moderation.pt @@ -11,7 +11,6 @@