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.
This commit is contained in:
Katja Suess 2019-12-05 21:55:23 +01:00
parent f7b8335d27
commit 084d2893e7
17 changed files with 684 additions and 534 deletions

3
news/164.enhancement Normal file
View File

@ -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]

View File

@ -39,8 +39,9 @@
portrait_url python:view.get_commenter_portrait(reply.author_username); portrait_url python:view.get_commenter_portrait(reply.author_username);
review_state python:wtool.getInfoFor(reply, 'review_state', 'none'); review_state python:wtool.getInfoFor(reply, 'review_state', 'none');
canEdit python:view.can_edit(reply); canEdit python:view.can_edit(reply);
canDelete python:view.can_delete(reply)" canDelete python:view.can_delete(reply);
tal:attributes="class python:'comment replyTreeLevel'+str(depth)+' state-'+str(review_state); 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}" id string:${reply/getId}"
tal:condition="python:canReview or review_state == 'published'"> tal:condition="python:canReview or review_state == 'published'">
@ -119,7 +120,7 @@
<tal:edit tal:condition="python:isEditCommentAllowed and canEdit"> <tal:edit tal:condition="python:isEditCommentAllowed and canEdit">
<!-- plone 5 will have auth_token available <!-- plone 5 will have auth_token available
so we'll use modal pattern --> so we'll use modal pattern -->
<a class="commentactionsform pat-plone-modal context" <a class="pat-plone-modal context"
tal:condition="auth_token" tal:condition="auth_token"
tal:attributes="href string:${reply/absolute_url}/@@edit-comment?_authenticator=${auth_token}" tal:attributes="href string:${reply/absolute_url}/@@edit-comment?_authenticator=${auth_token}"
i18n:translate="Edit">Edit</a> i18n:translate="Edit">Edit</a>
@ -146,10 +147,10 @@
class="commentactionsform" class="commentactionsform"
tal:condition="canReview" tal:condition="canReview"
tal:repeat="action reply_dict/actions|nothing" 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"> name action/id">
<input type="hidden" name="workflow_action" tal:attributes="value action/id" /> <input type="hidden" name="workflow_action" tal:attributes="value action/id" />
<input name="form.button.PublishComment" <input name="form.button.TransmitComment"
class="context" class="context"
type="submit" type="submit"
tal:attributes="value action/title" tal:attributes="value action/title"

View File

@ -91,23 +91,15 @@
permission="plone.app.discussion.DeleteOwnComments" permission="plone.app.discussion.DeleteOwnComments"
/> />
<!-- Publish comment view --> <!-- Comment Transition -->
<browser:page <browser:page
for="plone.app.discussion.interfaces.IComment" for="plone.app.discussion.interfaces.IComment"
name="moderate-publish-comment" name="transmit-comment"
layer="..interfaces.IDiscussionLayer" layer="..interfaces.IDiscussionLayer"
class=".moderation.PublishComment" class=".moderation.CommentTransition"
permission="plone.app.discussion.ReviewComments" permission="plone.app.discussion.ReviewComments"
/> />
<!-- Reject comment view -->
<browser:page
for="plone.app.discussion.interfaces.IComment"
name="moderate-reject-comment"
layer="..interfaces.IDiscussionLayer"
class=".moderation.RejectComment"
permission="plone.app.discussion.ReviewComments"
/>
<!-- Comments viewlet --> <!-- Comments viewlet -->
<browser:viewlet <browser:viewlet

View File

@ -177,15 +177,18 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
return True return True
def custom_comment_workflow_warning(self): def custom_comment_workflow_warning(self):
"""Returns a warning string if a custom comment workflow is enabled. """Return True if a custom comment workflow is enabled."""
"""
wftool = getToolByName(self.context, 'portal_workflow', None) wftool = getToolByName(self.context, 'portal_workflow', None)
workflow_chain = wftool.getChainForPortalType('Discussion Item') workflow_chain = wftool.getChainForPortalType('Discussion Item')
one_state_workflow_enabled = \ one_state_workflow_enabled = \
'comment_one_state_workflow' in workflow_chain 'comment_one_state_workflow' in workflow_chain
comment_review_workflow_enabled = \ comment_review_workflow_enabled = \
'comment_review_workflow' in workflow_chain 'comment_review_workflow' in workflow_chain
if one_state_workflow_enabled or comment_review_workflow_enabled: comment_multiple_state_review_workflow = \
'comment_multiple_state_review_workflow' in workflow_chain
if one_state_workflow_enabled \
or comment_review_workflow_enabled \
or comment_multiple_state_review_workflow:
return return
return True return True

View File

@ -162,9 +162,9 @@ require([ // jshint ignore:line
/********************************************************************** /**********************************************************************
* Publish a single comment. * Transmit a single comment.
**********************************************************************/ **********************************************************************/
$('input[name="form.button.PublishComment"]').on('click', function () { $('input[name="form.button.TransmitComment"]').on('click', function () {
var trigger = this; var trigger = this;
var form = $(this).parents('form'); var form = $(this).parents('form');
var data = $(form).serialize(); var data = $(form).serialize();
@ -176,7 +176,7 @@ require([ // jshint ignore:line
context: trigger, context: trigger,
success: function (msg) { // jshint ignore:line success: function (msg) { // jshint ignore:line
// remove button (trigger object can't be directly removed) // remove button (trigger object can't be directly removed)
form.find('input[name="form.button.PublishComment"]').remove(); form.find('input[name="form.button.TransmitComment"]').remove();
form.parents('.state-pending').toggleClass('state-pending').toggleClass('state-published'); form.parents('.state-pending').toggleClass('state-pending').toggleClass('state-published');
}, },
error: function (msg) { // jshint ignore:line error: function (msg) { // jshint ignore:line
@ -201,7 +201,7 @@ require([ // jshint ignore:line
/********************************************************************** /**********************************************************************
* Delete a comment and its answers. * Delete a comment and its answers.
**********************************************************************/ **********************************************************************/
$('input[name="form.button.DeleteComment"]').on('click', function () { $('input[name="form.button.DeleteCommentComment"]').on('click', function () {
var trigger = this; var trigger = this;
var form = $(this).parents('form'); var form = $(this).parents('form');
var data = $(form).serialize(); var data = $(form).serialize();

View File

@ -3,47 +3,39 @@
* jQuery functions for the plone.app.discussion bulk moderation. * jQuery functions for the plone.app.discussion bulk moderation.
* *
******************************************************************************/ ******************************************************************************/
/* global require, alert */
/* jshint quotmark: false */
if (require === undefined) { if (require === undefined) {
require = function(reqs, torun){ // jshint ignore:line require = function(reqs, torun) {
'use strict'; "use strict";
return torun(window.jQuery); return torun(window.jQuery);
}; };
} }
require([ // jshint ignore:line require(["jquery", "pat-registry"], function($, registry) {
'jquery' "use strict";
], function ($) {
'use strict';
// This unnamed function allows us to use $ inside of a block of code
// without permanently overwriting $.
// http://docs.jquery.com/Using_jQuery_with_Other_Libraries
//#JSCOVERAGE_IF 0
/**************************************************************************
* Document Ready Function: Executes when DOM is ready.
**************************************************************************/
$(document).ready(function() { $(document).ready(function() {
init();
});
function init() {
/********************************************************************** /**********************************************************************
* Delete a single comment. * Delete a single comment.
**********************************************************************/ **********************************************************************/
$("input[name='form.button.Delete']").click(function (e) { $("input[name='form.button.moderation.DeleteComment']").click(function(e) {
e.preventDefault(); e.preventDefault();
var row = $(this).parent().parent(); var row = $(this).closest("tr");
var path = $(row).find("[name='selected_obj_paths:list']").attr("value"); var path = row.find("[name='selected_obj_paths:list']").attr("value");
var auth_key = $('input[name="_authenticator"]').val(); var auth_key = $('input[name="_authenticator"]').val();
var target = path + "/@@moderate-delete-comment?_authenticator=" + auth_key; var target =
path + "/@@moderate-delete-comment?_authenticator=" + auth_key;
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: target, url: target,
success: function (msg) { // jshint ignore:line success: function(msg) {
// fade out row // fade out row
$(row).fadeOut("normal", function () { row.fadeOut(250).fadeIn(250, function() {
$(this).remove(); row.remove();
}); });
// reload page if all comments have been removed // reload page if all comments have been removed
var comments = $("table#review-comments > tbody > tr"); var comments = $("table#review-comments > tbody > tr");
@ -51,105 +43,87 @@ require([ // jshint ignore:line
location.reload(); location.reload();
} }
}, },
error: function (msg) { // jshint ignore:line error: function(msg) {
alert("Error sending AJAX request:" + target); alert("Error sending AJAX request:" + target);
} }
}); });
}); });
/********************************************************************** /**********************************************************************
* Publish a single comment. * Transmit a single comment.
**********************************************************************/ **********************************************************************/
$("input[name='form.button.Publish']").click(function (e) { $('input[name="form.button.moderation.TransmitComment"]').click(function(
e
) {
e.preventDefault(); e.preventDefault();
var row = $(this).parent().parent(); let button = $(this);
var path = $(row).find("[name='selected_obj_paths:list']").attr("value"); 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(); var auth_key = $('input[name="_authenticator"]').val();
var target = path + "/@@moderate-publish-comment?_authenticator=" + auth_key; // distinction of workflow_action
var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments"; var target =
path +
"/@@transmit-comment?_authenticator=" +
auth_key +
"&workflow_action=" +
workflow_action;
var moderate =
$(this)
.closest("fieldset")
.attr("id") == "fieldset-moderate-comments";
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: target, url: target,
success: function (msg) { // jshint ignore:line success: function(msg) {
if (moderate) { if (moderate) {
// fade out row let url = location.href;
$(row).fadeOut("normal", function () { $("#review-comments").load(url + " #review-comments", function() {
$(this).remove(); init();
$(".pat-plone-modal").patPloneModal();
}); });
// reload page if all comments have been removed
var comments = $("table#review-comments > tbody > tr");
if (comments.length === 1) {
location.reload();
}
} else { } else {
location.reload(); location.reload();
} }
}, },
error: function (msg) { // jshint ignore:line error: function(msg) {
alert("Error sending AJAX request:" + target); alert(
"Error transmitting comment. (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) * Bulk actions for comments (delete, publish)
**********************************************************************/ **********************************************************************/
$("input[name='form.button.BulkAction']").click(function(e) { $("input[name='form.button.BulkAction']").click(function(e) {
e.preventDefault(); e.preventDefault();
var form = $(this).parents("form"); var form = $(this).parents("form");
var target = $(form).attr('action'); var target = $(form).attr("action");
var params = $(form).serialize(); var params = $(form).serialize();
var valArray = $('input:checkbox:checked'); var valArray = $("input:checkbox:checked");
var selectField = $(form).find("[name='form.select.BulkAction']"); var selectField = $(form).find("[name='form.select.BulkAction']");
if (selectField.val() === '-1') { if (selectField.val() === "-1") {
// XXX: translate message // XXX: translate message
alert("You haven't selected a bulk action. Please select one."); alert("You haven't selected a bulk action. Please select one.");
} else if (valArray.length === 0) { } else if (valArray.length === 0) {
// XXX: translate message // XXX: translate message
alert("You haven't selected any comment for this bulk action." + alert(
"Please select at least one comment."); "You haven't selected any comment for this bulk action." +
"Please select at least one comment."
);
} else { } else {
$.post(target, params, function (data) { // jshint ignore:line $.post(target, params, function(data) {
valArray.each(function() { valArray.each(function() {
/* Remove all selected lines. */ /* Remove all selected lines. */
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
row.fadeOut("normal", function() { row.fadeOut("normal", function() {
row.remove(); row.remove();
}); });
@ -161,28 +135,53 @@ require([ // jshint ignore:line
} }
}); });
// reset the bulkaction select // reset the bulkaction select
selectField.find("option[value='-1']").attr('selected', 'selected'); selectField.find("option[value='-1']").attr("selected", "selected");
} }
}); });
/********************************************************************** /**********************************************************************
* Check or uncheck all checkboxes from the batch moderation page. * Check or uncheck all checkboxes from the batch moderation page.
**********************************************************************/ **********************************************************************/
$("input[name='check_all']").click(function() { $("input[name='check_all']").click(function() {
if ($(this).val() === '0') { if ($(this).val() === "0") {
$(this).parents("table") $(this)
.parents("table")
.find("input:checkbox") .find("input:checkbox")
.attr("checked", "checked"); .prop("checked", true);
$(this).val("1"); $(this).val("1");
} else { } else {
$(this).parents("table") $(this)
.parents("table")
.find("input:checkbox") .find("input:checkbox")
.attr("checked", ""); .prop("checked", false);
$(this).val("0"); $(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 text of a comment in the batch moderation page.
@ -190,34 +189,44 @@ require([ // jshint ignore:line
$(".show-full-comment-text").click(function(e) { $(".show-full-comment-text").click(function(e) {
e.preventDefault(); e.preventDefault();
var target = $(this).attr("href"); var target = $(this).attr("href");
var td = $(this).parent(); var parent = $(this).parent();
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: target, url: target,
data: "", data: "",
success: function(data) { success: function(data) {
// show full text // show full text
td.replaceWith("<td>" + data + "</td>"); parent.html(data);
}, },
error: function (msg) { // jshint ignore:line error: function(msg) {
alert("Error getting full comment text:" + target); alert("Error getting full comment text:" + target);
} }
}); });
}); });
/********************************************************************** /**********************************************************************
* Comments approved: Load history for approved date. * Comments approved: Load history for approved date.
**********************************************************************/ **********************************************************************/
$(".last-history-entry").each(function() { $(".last-history-entry").each(function() {
$(this).load($(this).attr("data-href") + " .historyByLine", function() { var me = $(this);
let currententry = $(this).children(".historyByLine").first(); $.ajax({
$(this).html(currententry); 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("<br/>");
});
// format date
registry.scan(me);
},
error: function(msg) {
alert("Error getting history.");
}
}); });
}); });
} // end init
});
//#JSCOVERAGE_ENDIF
}); });

View File

@ -17,7 +17,9 @@
b_size python:30; b_size python:30;
b_start python:0; b_start python:0;
b_start request/b_start | b_start; 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);
">
<script type="text/javascript" <script type="text/javascript"
tal:attributes="src string:${context/portal_url}/++plone++plone.app.discussion.javascripts/moderation.js"> tal:attributes="src string:${context/portal_url}/++plone++plone.app.discussion.javascripts/moderation.js">
</script> </script>
@ -40,16 +42,11 @@
</span> </span>
</div> </div>
<div class="pat-autotoc autotabs"
data-pat-autotoc="section:fieldset.tab;levels:legend;">
<fieldset class="tab">
<legend i18n:translate="heading_moderate_comments">
Moderate comments</legend>
<div>
<form tal:condition="not:items"> <form tal:condition="not:items">
<fieldset id="fieldset-moderate-comments" class="formPanel"> <fieldset id="fieldset-moderate-comments" class="formPanel">
<p id="no-comments-message" i18n:translate="message_nothing_to_moderate"> <p id="no-comments-message" i18n:translate="message_no_comments">
No comments to moderate. No comments
</p> </p>
</fieldset> </fieldset>
</form> </form>
@ -57,16 +54,31 @@
<form method="post" <form method="post"
action="#" action="#"
tal:attributes="action string:${context/absolute_url}/@@bulk-actions" tal:attributes="action string:${context/absolute_url}/@@bulk-actions"
tal:condition="items"
tal:define="batch python:Batch(items, b_size, int(b_start), orphan=1);"> tal:define="batch python:Batch(items, b_size, int(b_start), orphan=1);">
<fieldset id="fieldset-moderate-comments" class="formPanel"> <fieldset id="fieldset-moderate-comments" class="formPanel">
<div metal:use-macro="here/batch_macros/macros/navigation" /> <div metal:use-macro="here/batch_macros/macros/navigation" />
<table id="review-comments" class="listing"> <table id="review-comments" class="listing">
<thead> <thead>
<tr> <tr>
<th colspan="7">
<fieldset>
<input type="radio" id="all" name="review_state" value="all"
tal:attributes="checked python:request.review_state=='all'">
<label for="all" i18n:translate="">all</label>
<tal:workflow-filter tal:repeat="review_state python:['pending', 'published', 'rejected', 'spam']">
<input type="radio" name="review_state"
tal:attributes="
value review_state;
id review_state;
checked python:request.review_state==review_state">
<label tal:attributes="for review_state"><span tal:content="python:view.translate('comment_{}'.format(review_state))">review_state</span></label>
</tal:workflow-filter>
</fieldset>
</th>
</tr>
<tr tal:condition="items">
<th id="bulkactions" class="nosort" colspan="7"> <th id="bulkactions" class="nosort" colspan="7">
<select name="form.select.BulkAction"> <select name="form.select.BulkAction">
<option selected="selected" value="-1" i18n:translate="title_bulkactions">Bulk Actions</option> <option selected="selected" value="-1" i18n:translate="title_bulkactions">Bulk Actions</option>
@ -84,20 +96,23 @@
i18n:attributes="value label_apply;" /> i18n:attributes="value label_apply;" />
</th> </th>
</tr> </tr>
<tr> <tr tal:condition="items">
<th class="nosort"><input name="check_all" type="checkbox" value="0" /></th> <th class="nosort"><input name="check_all" type="checkbox" value="0" /></th>
<th class="nosort" i18n:translate="heading_commenter">Commenter</th> <th class="nosort" i18n:translate="heading_commenter">Commenter</th>
<th class="nosort" i18n:translate="heading_date">Date</th> <th class="nosort" i18n:translate="heading_date">Date</th>
<th class="nosort" i18n:translate="heading_in_reponse_to">In Response To</th> <th class="nosort" i18n:translate="heading_in_reponse_to">In Response To</th>
<th class="nosort" i18n:translate="heading_comment">Comment</th> <th class="nosort" i18n:translate="heading_comment">Comment</th>
<th class="nosort" i18n:translate="heading_action">Action</th> <th class="nosort" i18n:translate="heading_changedby">Last Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tal:block repeat="item batch"> <tal:block repeat="item batch"
tal:condition="items">
<tr class="commentrow" <tr class="commentrow"
tal:define="even repeat/item/even; tal:define="even repeat/item/even;
email python:getattr(item.getObject(), 'author_email');" item_obj item/getObject;
email item_obj/author_email;
item_url item/getURL;"
tal:attributes="class python: even and 'odd' or 'even'"> tal:attributes="class python: even and 'odd' or 'even'">
<td class="notDraggable"> <td class="notDraggable">
<input type="checkbox" <input type="checkbox"
@ -121,116 +136,55 @@
</a> </a>
</tal:email> </tal:email>
</td> </td>
<td tal:content="python:toLocalizedTime(item.created, long_format=1)"/> <td style="white-space: nowrap;" tal:content="python:toLocalizedTime(item.created, long_format=1)"/>
<td> <td>
<a tal:attributes="href item/getURL" <a tal:attributes="href item_url" target="_blank"
tal:content="item/in_response_to" /> tal:content="item/in_response_to" />
</td> </td>
<td> <td tal:attributes="class python:colorclass(item.review_state)">
<span tal:replace="item/Description" />
<a href=""
tal:attributes="href string:${item/getURL}/getText"
tal:condition="python:item.Description.endswith('[...]')"
i18n:translate="label_show_full_comment_text"
class="show-full-comment-text">show full comment text</a>
</td>
<td class="actions">
<input id=""
class="context comment-publish-button"
type="submit"
value="Approve"
name="form.button.Publish"
i18n:attributes="value label_publish;"
tal:attributes="id item/id"
tal:condition="python:item.review_state in ['pending',]"
/>
<input id=""
class="context comment-reject-button"
type="submit"
value="Reject"
name="form.button.Reject"
i18n:attributes="value label_reject;"
tal:attributes="id item/id"
tal:condition="python:view.moderation_3state and item.review_state == 'pending'"
/>
<input id=""
class="destructive comment-delete-button"
type="submit"
value="Delete"
name="form.button.Delete"
i18n:attributes="value label_delete;"
tal:attributes="id item/id"
/>
</td>
</tr>
</tal:block>
</tbody>
</table>
<div metal:use-macro="here/batch_macros/macros/navigation" />
</fieldset>
</form>
</div>
</fieldset>
<fieldset class="tab">
<legend i18n:translate="heading_approved_and_rejected_comments">
Approved comments</legend>
<div> <div>
<form tal:condition="not:items_approved_or_rejected">
<fieldset id="fieldset-moderated-comments" class="formPanel">
<p id="no-comments-message" i18n:translate="message_no_comments_approved">
No comments approved
</p>
</fieldset>
</form>
<form method="post"
action="#"
tal:condition="items_approved_or_rejected"
tal:define="batch python:Batch(items_approved_or_rejected, b_size, int(b_start), orphan=1);">
<fieldset id="fieldset-moderated-comments" class="formPanel">
<div metal:use-macro="here/batch_macros/macros/navigation" />
<table id="review-comments" class="listing">
<thead>
<tr>
<th class="nosort" i18n:translate="heading_commenter">Commenter</th>
<th class="nosort" i18n:translate="heading_date">Date</th>
<th class="nosort" i18n:translate="heading_in_reponse_to">In Response To</th>
<th class="nosort" i18n:translate="heading_comment">Comment</th>
<th class="nosort" i18n:translate="heading_changedby">Last Action</th>
<th class="nosort" i18n:translate="heading_action">Action</th>
</tr>
</thead>
<tbody>
<tal:block repeat="item batch">
<tr class="commentrow"
tal:define="even repeat/item/even;
email python:getattr(item.getObject(), 'author_email');
item_url item/getURL;"
tal:attributes="class python: even and 'odd' or 'even'">
<td>
<span tal:content="python:item.author_name or item.Creator">Name</span>
<tal:email tal:condition="email">
<br/>
<a tal:attributes="href string:mailto:$email;"
tal:content="email">Email
</a>
</tal:email>
</td>
<td tal:content="python:toLocalizedTime(item.created, long_format=1)"/>
<td>
<a tal:attributes="href item_url"
tal:content="item/in_response_to" />
</td>
<td tal:attributes="class python:item.review_state=='rejected' and 'state-private'">
<span tal:replace="item/Description"/> <span tal:replace="item/Description"/>
<a href="" <a href=""
tal:attributes="href string:$item_url/getText" tal:attributes="href string:$item_url/getText"
tal:condition="python:item.Description.endswith('[...]')" tal:condition="python:item.Description.endswith('[...]')"
i18n:translate="label_show_full_comment_text" i18n:translate="label_show_full_comment_text"
class="show-full-comment-text">show full comment text</a> class="show-full-comment-text">show full comment text</a>
</div>
<div class="actions input-group-addon">
<input
type="hidden"
name="selected_obj_paths:list"
value="#"
tal:attributes="value item/getURL"
/>
<!-- delete -->
<input id=""
class="destructive comment-delete-button"
type="submit"
value="Delete"
name="form.button.moderation.DeleteComment"
i18n:attributes="value label_delete;"
tal:attributes="id item/id"
/>
<!-- edit -->
<a class="pat-plone-modal context" href="#"
tal:attributes="href python:item_url+'/@@edit-comment?review_state=' + item.review_state">Edit</a>
<!-- workflow actions -->
<tal:transitions
tal:define="
transitions python:view.allowed_transitions(item_obj)">
<input name="form.button.moderation.TransmitComment"
tal:repeat="transition transitions"
class="context"
type="submit"
value="Label"
tal:attributes="id item/id;
data-transition transition/id;
value python:view.translate(transition['title'])"
/>
</tal:transitions>
</div>
</td> </td>
<td> <td>
<span class="last-history-entry" <span class="last-history-entry"
@ -238,36 +192,6 @@
last history entry last history entry
</span> </span>
</td> </td>
<td class="actions">
<input type="hidden" name="selected_obj_paths:list" value="#"
tal:attributes="value item/getURL" />
<input id=""
class="context comment-publish-button"
type="submit"
value="Approve"
name="form.button.Publish"
i18n:attributes="value label_publish;"
tal:attributes="id item/id"
tal:condition="python:item.review_state in ['pending', 'rejected']"
/>
<input id=""
class="context comment-reject-button"
type="submit"
value="Reject"
name="form.button.Reject"
i18n:attributes="value label_reject;"
tal:attributes="id item/id"
tal:condition="python:view.moderation_3state and item.review_state == 'published'"
/>
<input id=""
class="destructive comment-delete-button"
type="submit"
value="Delete"
name="form.button.Delete"
i18n:attributes="value label_delete;"
tal:attributes="id item/id"
/>
</td>
</tr> </tr>
</tal:block> </tal:block>
</tbody> </tbody>
@ -276,10 +200,6 @@
</fieldset> </fieldset>
</form> </form>
</div>
</fieldset>
</div>
</tal:main-macro> </tal:main-macro>
</metal:main> </metal:main>

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # coding: utf-8
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
from AccessControl import Unauthorized from AccessControl import Unauthorized
from Acquisition import aq_inner from Acquisition import aq_inner
@ -17,9 +17,23 @@ from zope.event import notify
from zope.interface import alsoProvides 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): class View(BrowserView):
"""Comment moderation view. """Show comment moderation view."""
"""
template = ViewPageTemplateFile('moderation.pt') template = ViewPageTemplateFile('moderation.pt')
try: try:
template.id = '@@moderate-comments' template.id = '@@moderate-comments'
@ -27,53 +41,91 @@ class View(BrowserView):
# id is not writeable in Zope 2.12 # id is not writeable in Zope 2.12
pass pass
def __init__(self, context, request):
self.context = context
self.request = request
self.workflowTool = getToolByName(self.context, 'portal_workflow')
def __call__(self): def __call__(self):
self.request.set('disable_border', True) self.request.set('disable_border', True)
context = aq_inner(self.context) self.request.set('review_state',
catalog = getToolByName(context, 'portal_catalog') self.request.get('review_state', 'pending'))
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')
return self.template() return self.template()
def moderation_enabled(self): def comments(self):
"""Returns true if a 'review workflow' is enabled on 'Discussion Item' """Return comments of defined review_state.
content type. A 'review workflow' is characterized by implementing
a 'pending' workflow state. review_state is string or list of strings.
""" """
context = aq_inner(self.context) catalog = getToolByName(self.context, 'portal_catalog')
workflowTool = getToolByName(context, 'portal_workflow') if self.request.review_state == 'all':
comment_workflow = workflowTool.getChainForPortalType( 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') 'Discussion Item')
if comment_workflow: if comment_workflow:
comment_workflow = comment_workflow[0] comment_workflow = comment_workflow[0]
comment_workflow = workflowTool[comment_workflow] comment_workflow = self.workflowTool[comment_workflow]
if 'pending' in comment_workflow.states: if 'pending' in comment_workflow.states:
return True return True
return False return False
@property @property
def moderation_3state(self): def moderation_multiple_state_enabled(self):
"""Returns true if a 'review 3 state workflow' is enabled on 'Discussion Item' """Return true if a 'review multiple state workflow' is enabled on
content type. A 'review 3 state workflow' is characterized by implementing 'Discussion Item' content type.
a 'rejected' workflow state.
A 'review multipe state workflow' is characterized by implementing
a 'rejected' workflow state and a 'spam' workflow state.
""" """
context = aq_inner(self.context) comment_workflow = self.workflowTool.getChainForPortalType(
workflowTool = getToolByName(context, 'portal_workflow')
comment_workflow = workflowTool.getChainForPortalType(
'Discussion Item') 'Discussion Item')
if comment_workflow: if comment_workflow:
comment_workflow = comment_workflow[0] comment_workflow = comment_workflow[0]
comment_workflow = workflowTool[comment_workflow] comment_workflow = self.workflowTool[comment_workflow]
if 'rejected' in comment_workflow.states: if 'rejected' in comment_workflow.states:
return True return True
return False 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': <TransitionDefinition at /PloneRejected/portal_workflow/comment_multiple_state_review_workflow/transitions/mark_as_spam>, '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': <TransitionDefinition at /PloneRejected/portal_workflow/comment_multiple_state_review_workflow/transitions/publish>,
'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': <TransitionDefinition at /PloneRejected/portal_workflow/comment_multiple_state_review_workflow/transitions/reject>, '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): class ModerateCommentsEnabled(BrowserView):
@ -181,13 +233,13 @@ class DeleteOwnComment(DeleteComment):
raise Unauthorized("You're not allowed to delete this comment.") raise Unauthorized("You're not allowed to delete this comment.")
class PublishComment(BrowserView): class CommentTransition(BrowserView):
"""Publish a comment. r"""Publish, reject, recall a comment or mark it as spam.
This view is always called directly on the comment object: This view is always called directly on the comment object:
http://nohost/front-page/++conversation++default/1286289644723317/\ http://nohost/front-page/++conversation++default/1286289644723317/\
@@moderate-publish-comment @@transmit-comment
Each table row (comment) in the moderation view contains a hidden input Each table row (comment) in the moderation view contains a hidden input
field with the absolute URL of the content object: field with the absolute URL of the content object:
@ -203,31 +255,39 @@ class PublishComment(BrowserView):
""" """
def __call__(self): def __call__(self):
alsoProvides(self.request, IDisableCSRFProtection) """Call CommentTransition."""
comment = aq_inner(self.context) comment = aq_inner(self.context)
content_object = aq_parent(aq_parent(comment)) content_object = aq_parent(aq_parent(comment))
print("*** called: PublishComment for ", comment.Description) print("*** called: PublishComment for ", comment.Description)
workflowTool = getToolByName(comment, 'portal_workflow', None) workflowTool = getToolByName(comment, 'portal_workflow', None)
workflow_action = self.request.form.get('workflow_action', 'publish') workflow_action = self.request.form.get('workflow_action', 'publish')
review_state = workflowTool.getInfoFor(comment, 'review_state', '') review_state = workflowTool.getInfoFor(comment, 'review_state', '')
if review_state != "published":
workflowTool.doActionFor(comment, workflow_action) workflowTool.doActionFor(comment, workflow_action)
comment.reindexObject() comment.reindexObject()
content_object.reindexObject(idxs=['total_comments']) content_object.reindexObject(idxs=['total_comments'])
notify(CommentPublishedEvent(self.context, comment)) 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( IStatusMessage(self.context.REQUEST).addStatusMessage(
_('Comment approved.'), translated, type='info')
type='info')
else:
IStatusMessage(self.context.REQUEST).addStatusMessage(
_('Comment already approved.'),
type='info')
came_from = self.context.REQUEST.HTTP_REFERER came_from = self.context.REQUEST.HTTP_REFERER
# if the referrer already has a came_from in it, don't redirect back # 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 if (len(came_from) == 0
not getToolByName( or 'came_from=' in came_from
content_object, 'portal_url').isURLInPortal(came_from) or or not getToolByName(
'@@confirm-action' in came_from): content_object, 'portal_url').isURLInPortal(came_from)):
came_from = content_object.absolute_url() came_from = content_object.absolute_url()
return self.context.REQUEST.RESPONSE.redirect(came_from) return self.context.REQUEST.RESPONSE.redirect(came_from)
@ -268,7 +328,7 @@ class RejectComment(BrowserView):
class BulkActionsView(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 Each table row of the moderation view has a checkbox with the absolute
path (without host and port) of the comment objects: path (without host and port) of the comment objects:
@ -291,6 +351,7 @@ class BulkActionsView(BrowserView):
""" """
def __call__(self): def __call__(self):
"""Call BulkActionsView."""
if 'form.select.BulkAction' in self.request: if 'form.select.BulkAction' in self.request:
bulkaction = self.request.get('form.select.BulkAction') bulkaction = self.request.get('form.select.BulkAction')
self.paths = self.request.get('paths') self.paths = self.request.get('paths')

View File

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

View File

@ -1,7 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<object name="portal_workflow" meta_type="Plone Workflow Tool"> <object name="portal_workflow" meta_type="Plone Workflow Tool">
<object name="comment_review_workflow" meta_type="Workflow"/> <object name="comment_review_workflow" meta_type="Workflow"/>
<object name="comment_3state_review_workflow" meta_type="Workflow"/> <object name="comment_multiple_state_review_workflow" meta_type="Workflow"/>
<object name="comment_one_state_workflow" meta_type="Workflow"/> <object name="comment_one_state_workflow" meta_type="Workflow"/>
<bindings> <bindings>
<type type_id="Discussion Item"> <type type_id="Discussion Item">

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<dc-workflow
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone"
workflow_id="comment_multiple_state_review_workflow"
title="Comment Multiple State Review Workflow"
description="A review workflow for comments with multiple states"
state_variable="review_state"
initial_state="pending"
i18n:attributes="title; description"
>
<permission>Access contents information</permission>
<permission>Modify portal content</permission>
<permission>Reply to item</permission>
<permission>View</permission>
<state state_id="pending" title="Pending">
<description>Submitted, pending review.</description>
<exit-transition transition_id="mark_as_spam"/>
<exit-transition transition_id="publish"/>
<exit-transition transition_id="reject"/>
<permission-map name="Access contents information" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Owner</permission-role>
<permission-role>Reviewer</permission-role>
</permission-map>
<permission-map name="Modify portal content" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Owner</permission-role>
<permission-role>Reviewer</permission-role>
</permission-map>
<permission-map name="Reply to item" acquired="False">
</permission-map>
<permission-map name="View" acquired="False">
<permission-role>Manager</permission-role>
<permission-role>Owner</permission-role>
<permission-role>Reviewer</permission-role>
</permission-map>
</state>
<state state_id="published" title="Published">
<description>Visible to everyone, non-editable.</description>
<exit-transition transition_id="mark_as_spam"/>
<exit-transition transition_id="recall"/>
<exit-transition transition_id="reject"/>
<permission-map name="Access contents information" acquired="True">
</permission-map>
<permission-map name="Modify portal content" acquired="False">
<permission-role>Manager</permission-role>
</permission-map>
<permission-map name="Reply to item" acquired="True">
</permission-map>
<permission-map name="View" acquired="True">
</permission-map>
</state>
<state state_id="rejected" title="Rejected">
<exit-transition transition_id="mark_as_spam"/>
<exit-transition transition_id="publish"/>
<exit-transition transition_id="recall"/>
</state>
<state state_id="spam" title="Spam">
<exit-transition transition_id="publish"/>
<exit-transition transition_id="recall"/>
<exit-transition transition_id="reject"/>
</state>
<transition transition_id="mark_as_spam" title="Mark as spam" new_state="spam" trigger="USER" before_script="" after_script="">
<description>Spam comments are invisible to other users.</description>
<action url="%(content_url)s/content_status_modify?workflow_action=mark_as_spam" category="workflow" icon="">Spam</action>
<guard>
<guard-permission>Review comments</guard-permission>
</guard>
</transition>
<transition transition_id="publish" title="Reviewer approves content" new_state="published" trigger="USER" before_script="" after_script="">
<description>Approving the comment makes it visible to other users.</description>
<action url="%(content_url)s/content_status_modify?workflow_action=publish" category="workflow" icon="">Approve</action>
<guard>
<guard-permission>Review comments</guard-permission>
</guard>
</transition>
<transition transition_id="recall" title="Reviewer recalls comment back to pending state" new_state="pending" trigger="USER" before_script="" after_script="">
<action url="%(content_url)s/content_status_modify?workflow_action=recall" category="workflow" icon="">Reject</action>
<guard>
<guard-permission>Review comments</guard-permission>
</guard>
</transition>
<transition transition_id="reject" title="Reviewer rejects comment" new_state="rejected" trigger="USER" before_script="" after_script="">
<description>Rejected comments are invisible to other users.</description>
<action url="%(content_url)s/content_status_modify?workflow_action=reject" category="workflow" icon="">Reject</action>
<guard>
<guard-permission>Review comments</guard-permission>
</guard>
</transition>
<variable variable_id="action" for_catalog="False" for_status="True" update_always="True">
<description>Previous transition</description>
<default>
<expression>transition/getId|nothing</expression>
</default>
<guard>
</guard>
</variable>
<variable variable_id="actor" for_catalog="False" for_status="True" update_always="True">
<description>The ID of the user who performed the previous transition</description>
<default>
<expression>user/getUserName</expression>
</default>
<guard>
</guard>
</variable>
<variable variable_id="comments" for_catalog="False" for_status="True" update_always="True">
<description>Comment about the last transition</description>
<default>
<expression>python:state_change.kwargs.get('comment', '')</expression>
</default>
<guard>
</guard>
</variable>
<variable variable_id="review_history" for_catalog="False" for_status="False" update_always="False">
<description>Provides access to workflow history</description>
<default>
<expression>state_change/getHistory</expression>
</default>
<guard>
<guard-permission>Request review</guard-permission>
<guard-permission>Review portal content</guard-permission>
</guard>
</variable>
<variable variable_id="time" for_catalog="False" for_status="True" update_always="True">
<description>When the previous transition was performed</description>
<default>
<expression>state_change/getDateTime</expression>
</default>
<guard>
</guard>
</variable>
</dc-workflow>

View File

@ -112,7 +112,7 @@ Administrators can see all posts and comment actions
>>> 'form.button.DeleteComment' in browser.contents >>> 'form.button.DeleteComment' in browser.contents
True True
>>> 'form.button.PublishComment' in browser.contents >>> 'form.button.TransmitComment' in browser.contents
True True
Anonymous user can not see any posts or comment actions 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 >>> 'form.button.DeleteComment' in unprivileged_browser.contents
False False
>>> 'form.button.PublishComment' in unprivileged_browser.contents >>> 'form.button.TransmitComment' in unprivileged_browser.contents
False False
The catalog does not list the comments yet: The catalog does not list the comments yet:
@ -156,7 +156,7 @@ actions.
>>> 'form.button.DeleteComment' in browser.contents >>> 'form.button.DeleteComment' in browser.contents
True True
>>> 'form.button.PublishComment' in browser.contents >>> 'form.button.TransmitComment' in browser.contents
True True

View File

@ -326,19 +326,19 @@ Deleting existing comments | 'Delete comments' permission
Anonymous cannot delete comments Anonymous cannot delete comments
>>> unprivileged_browser.open(urldoc1) >>> unprivileged_browser.open(urldoc1)
>>> 'form.button.Delete' in unprivileged_browser.contents >>> 'form.button.DeleteComment' in unprivileged_browser.contents
False False
A member cannot delete his own comments if he can't review or he isn't a Site Administrator A member cannot delete his own comments if he can't review or he isn't a Site Administrator
>>> browser_member.open(urldoc1) >>> browser_member.open(urldoc1)
>>> 'form.button.Delete' in browser_member.contents >>> 'form.button.DeleteComment' in browser_member.contents
False False
Admin can delete comments Admin can delete comments
>>> browser.open(urldoc1) >>> browser.open(urldoc1)
>>> 'form.button.Delete' in browser.contents >>> 'form.button.DeleteComment' in browser.contents
True True
Extract the delete comment url from the first "delete comment" button Extract the delete comment url from the first "delete comment" button

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from plone.app.discussion.browser.moderation import BulkActionsView from plone.app.discussion.browser.moderation import BulkActionsView
from plone.app.discussion.browser.moderation import DeleteComment 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.browser.moderation import View
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import IDiscussionSettings
@ -195,14 +195,14 @@ class RedirectionTest(unittest.TestCase):
def test_regression(self): def test_regression(self):
page_url = self.page.absolute_url() page_url = self.page.absolute_url()
self.request['HTTP_REFERER'] = page_url self.request['HTTP_REFERER'] = page_url
for Klass in (DeleteComment, PublishComment): for Klass in (DeleteComment, CommentTransition):
view = Klass(self.comment, self.request) view = Klass(self.comment, self.request)
view.__parent__ = self.comment view.__parent__ = self.comment
self.assertEqual(page_url, view()) self.assertEqual(page_url, view())
def test_valid_next_url(self): def test_valid_next_url(self):
self.request['HTTP_REFERER'] = 'http://attacker.com' self.request['HTTP_REFERER'] = 'http://attacker.com'
for Klass in (DeleteComment, PublishComment): for Klass in (DeleteComment, CommentTransition):
view = Klass(self.comment, self.request) view = Klass(self.comment, self.request)
view.__parent__ = self.comment view.__parent__ = self.comment
self.assertNotEqual('http://attacker.com', view()) self.assertNotEqual('http://attacker.com', view())

View File

@ -272,7 +272,7 @@ class CommentReviewWorkflowTest(unittest.TestCase):
'review_state', 'review_state',
), ),
) )
view = self.comment.restrictedTraverse('@@moderate-publish-comment') view = self.comment.restrictedTraverse('@@transmit-comment')
view() view()
self.assertEqual( self.assertEqual(
'published', 'published',
@ -295,7 +295,7 @@ class CommentReviewWorkflowTest(unittest.TestCase):
self.assertRaises( self.assertRaises(
Unauthorized, Unauthorized,
self.comment.restrictedTraverse, self.comment.restrictedTraverse,
'@@moderate-publish-comment', '@@transmit-comment',
) )
self.assertEqual( self.assertEqual(
'pending', 'pending',

View File

@ -20,7 +20,7 @@ def update_rolemap(context):
context.runImportStepFromProfile(default_profile, 'rolemap') 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 # 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. # 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 # 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' orig_chain[idx] = 'comment_one_state_workflow'
# Restore the chain. # Restore the chain.
wf_tool.setChainForPortalTypes([portal_type], orig_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 # Now go over the comments, update their role mappings, and reindex the
# allowedRolesAndUsers index. # allowedRolesAndUsers index.
portal_type = 'Discussion Item'
catalog = getToolByName(context, 'portal_catalog') 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): for brain in catalog.unrestrictedSearchResults(portal_type=portal_type):
try: try:
comment = brain.getObject() comment = brain.getObject()
@ -63,5 +66,14 @@ def upgrade_comment_workflows(context):
logger.info('Could not reindex comment {0}'.format(brain.getURL())) 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): def add_js_to_plone_legacy(context):
context.runImportStepFromProfile(default_profile, 'plone.app.registry') context.runImportStepFromProfile(default_profile, 'plone.app.registry')
def add_multiple_state_workflow(context):
upgrade_comment_workflows_retain_current_workflow(context)

View File

@ -62,4 +62,15 @@
/> />
</genericsetup:upgradeSteps> </genericsetup:upgradeSteps>
<genericsetup:upgradeSteps
source="1001"
destination="1002"
profile="plone.app.discussion:default">
<genericsetup:upgradeStep
title="Additional workflow with states pending, published, rejected and spam"
description="Additional states allows moderators to review history of publishing and rejection"
handler=".upgrades.add_multiple_state_workflow"
/>
</genericsetup:upgradeSteps>
</configure> </configure>