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,221 +3,230 @@
* 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() {
init();
/************************************************************************** });
* Document Ready Function: Executes when DOM is ready.
**************************************************************************/
$(document).ready(function () {
/**********************************************************************
* Delete a single comment.
**********************************************************************/
$("input[name='form.button.Delete']").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-delete-comment?_authenticator=" + auth_key;
$.ajax({
type: "GET",
url: target,
success: function (msg) { // jshint ignore:line
// 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();
}
},
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("<td>" + data + "</td>");
},
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);
});
});
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("<br/>");
});
// format date
registry.scan(me);
},
error: function(msg) {
alert("Error getting history.");
}
});
});
} // end init
}); });

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,245 +42,163 @@
</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">
<fieldset id="fieldset-moderate-comments" class="formPanel">
<p id="no-comments-message" i18n:translate="message_nothing_to_moderate">
No comments to moderate.
</p>
</fieldset>
</form>
<form method="post" <form tal:condition="not:items">
action="#" <fieldset id="fieldset-moderate-comments" class="formPanel">
tal:attributes="action string:${context/absolute_url}/@@bulk-actions" <p id="no-comments-message" i18n:translate="message_no_comments">
tal:condition="items" No comments
tal:define="batch python:Batch(items, b_size, int(b_start), orphan=1);"> </p>
</fieldset>
</form>
<fieldset id="fieldset-moderate-comments" class="formPanel"> <form method="post"
action="#"
tal:attributes="action string:${context/absolute_url}/@@bulk-actions"
tal:define="batch python:Batch(items, b_size, int(b_start), orphan=1);">
<div metal:use-macro="here/batch_macros/macros/navigation" /> <fieldset id="fieldset-moderate-comments" class="formPanel">
<table id="review-comments" class="listing"> <div metal:use-macro="here/batch_macros/macros/navigation" />
<thead> <table id="review-comments" class="listing">
<tr> <thead>
<th id="bulkactions" class="nosort" colspan="7"> <tr>
<select name="form.select.BulkAction"> <th colspan="7">
<option selected="selected" value="-1" i18n:translate="title_bulkactions">Bulk Actions</option> <fieldset>
<option value="publish" <input type="radio" id="all" name="review_state" value="all"
i18n:translate="bulkactions_publish" tal:attributes="checked python:request.review_state=='all'">
tal:condition="python: filter != 'published' and moderation_enabled">Approve</option> <label for="all" i18n:translate="">all</label>
<option value="delete" i18n:translate="bulkactions_delete">Delete</option> <tal:workflow-filter tal:repeat="review_state python:['pending', 'published', 'rejected', 'spam']">
</select> <input type="radio" name="review_state"
<input type="hidden" name="form.button.Filter" tal:attributes="value filter" value="" /> tal:attributes="
<input id="dobulkaction" value review_state;
type="submit" id review_state;
class="standalone allowMultiSubmit" checked python:request.review_state==review_state">
value="Apply" <label tal:attributes="for review_state"><span tal:content="python:view.translate('comment_{}'.format(review_state))">review_state</span></label>
name="form.button.BulkAction" </tal:workflow-filter>
i18n:attributes="value label_apply;" /> </fieldset>
</th> </th>
</tr> </tr>
<tr> <tr tal:condition="items">
<th class="nosort"><input name="check_all" type="checkbox" value="0" /></th> <th id="bulkactions" class="nosort" colspan="7">
<th class="nosort" i18n:translate="heading_commenter">Commenter</th> <select name="form.select.BulkAction">
<th class="nosort" i18n:translate="heading_date">Date</th> <option selected="selected" value="-1" i18n:translate="title_bulkactions">Bulk Actions</option>
<th class="nosort" i18n:translate="heading_in_reponse_to">In Response To</th> <option value="publish"
<th class="nosort" i18n:translate="heading_comment">Comment</th> i18n:translate="bulkactions_publish"
<th class="nosort" i18n:translate="heading_action">Action</th> tal:condition="python: filter != 'published' and moderation_enabled">Approve</option>
</tr> <option value="delete" i18n:translate="bulkactions_delete">Delete</option>
</thead> </select>
<tbody> <input type="hidden" name="form.button.Filter" tal:attributes="value filter" value="" />
<tal:block repeat="item batch"> <input id="dobulkaction"
<tr class="commentrow" type="submit"
tal:define="even repeat/item/even; class="standalone allowMultiSubmit"
email python:getattr(item.getObject(), 'author_email');" value="Apply"
tal:attributes="class python: even and 'odd' or 'even'"> name="form.button.BulkAction"
<td class="notDraggable"> i18n:attributes="value label_apply;" />
<input type="checkbox" </th>
class="noborder" </tr>
name="paths:list" id="#" <tr tal:condition="items">
value="#" <th class="nosort"><input name="check_all" type="checkbox" value="0" /></th>
tal:attributes="value item/getPath; <th class="nosort" i18n:translate="heading_commenter">Commenter</th>
id string:cb_${item/id}; <th class="nosort" i18n:translate="heading_date">Date</th>
checked item/checked|nothing; <th class="nosort" i18n:translate="heading_in_reponse_to">In Response To</th>
alt string:Select ${item/Title}; <th class="nosort" i18n:translate="heading_comment">Comment</th>
title string:Select ${item/Title}" /> <th class="nosort" i18n:translate="heading_changedby">Last Action</th>
<input type="hidden" name="selected_obj_paths:list" value="#" </tr>
tal:attributes="value item/getURL" /> </thead>
</td> <tbody>
<td> <tal:block repeat="item batch"
<span tal:content="python:item.author_name or item.Creator">Name</span> tal:condition="items">
<tal:email tal:condition="email"> <tr class="commentrow"
<br/> tal:define="even repeat/item/even;
<a tal:attributes="href string:mailto:$email;" item_obj item/getObject;
tal:content="email">Email email item_obj/author_email;
</a> item_url item/getURL;"
</tal:email> tal:attributes="class python: even and 'odd' or 'even'">
</td> <td class="notDraggable">
<td tal:content="python:toLocalizedTime(item.created, long_format=1)"/> <input type="checkbox"
<td> class="noborder"
<a tal:attributes="href item/getURL" name="paths:list" id="#"
tal:content="item/in_response_to" /> value="#"
</td> tal:attributes="value item/getPath;
<td> id string:cb_${item/id};
<span tal:replace="item/Description" /> checked item/checked|nothing;
<a href="" alt string:Select ${item/Title};
tal:attributes="href string:${item/getURL}/getText" title string:Select ${item/Title}" />
tal:condition="python:item.Description.endswith('[...]')" <input type="hidden" name="selected_obj_paths:list" value="#"
i18n:translate="label_show_full_comment_text" tal:attributes="value item/getURL" />
class="show-full-comment-text">show full comment text</a> </td>
</td> <td>
<td class="actions"> <span tal:content="python:item.author_name or item.Creator">Name</span>
<input id="" <tal:email tal:condition="email">
class="context comment-publish-button" <br/>
type="submit" <a tal:attributes="href string:mailto:$email;"
value="Approve" tal:content="email">Email
name="form.button.Publish" </a>
i18n:attributes="value label_publish;" </tal:email>
tal:attributes="id item/id" </td>
tal:condition="python:item.review_state in ['pending',]" <td style="white-space: nowrap;" tal:content="python:toLocalizedTime(item.created, long_format=1)"/>
/> <td>
<input id="" <a tal:attributes="href item_url" target="_blank"
class="context comment-reject-button" tal:content="item/in_response_to" />
type="submit" </td>
value="Reject" <td tal:attributes="class python:colorclass(item.review_state)">
name="form.button.Reject" <div>
i18n:attributes="value label_reject;" <span tal:replace="item/Description"/>
tal:attributes="id item/id" <a href=""
tal:condition="python:view.moderation_3state and item.review_state == 'pending'" tal:attributes="href string:$item_url/getText"
/> tal:condition="python:item.Description.endswith('[...]')"
<input id="" i18n:translate="label_show_full_comment_text"
class="destructive comment-delete-button" class="show-full-comment-text">show full comment text</a>
type="submit" </div>
value="Delete" <div class="actions input-group-addon">
name="form.button.Delete" <input
i18n:attributes="value label_delete;" type="hidden"
tal:attributes="id item/id" name="selected_obj_paths:list"
/> value="#"
</td> tal:attributes="value item/getURL"
</tr> />
</tal:block> <!-- delete -->
</tbody> <input id=""
</table> class="destructive comment-delete-button"
<div metal:use-macro="here/batch_macros/macros/navigation" /> type="submit"
</fieldset> value="Delete"
</form> name="form.button.moderation.DeleteComment"
</div> i18n:attributes="value label_delete;"
</fieldset> tal:attributes="id item/id"
<fieldset class="tab"> />
<legend i18n:translate="heading_approved_and_rejected_comments"> <!-- edit -->
Approved comments</legend> <a class="pat-plone-modal context" href="#"
<div> tal:attributes="href python:item_url+'/@@edit-comment?review_state=' + item.review_state">Edit</a>
<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" <!-- workflow actions -->
action="#" <tal:transitions
tal:condition="items_approved_or_rejected" tal:define="
tal:define="batch python:Batch(items_approved_or_rejected, b_size, int(b_start), orphan=1);"> transitions python:view.allowed_transitions(item_obj)">
<input name="form.button.moderation.TransmitComment"
<fieldset id="fieldset-moderated-comments" class="formPanel"> tal:repeat="transition transitions"
class="context"
<div metal:use-macro="here/batch_macros/macros/navigation" /> type="submit"
value="Label"
<table id="review-comments" class="listing"> tal:attributes="id item/id;
<thead> data-transition transition/id;
<tr> value python:view.translate(transition['title'])"
<th class="nosort" i18n:translate="heading_commenter">Commenter</th> />
<th class="nosort" i18n:translate="heading_date">Date</th> </tal:transitions>
<th class="nosort" i18n:translate="heading_in_reponse_to">In Response To</th> </div>
<th class="nosort" i18n:translate="heading_comment">Comment</th> </td>
<th class="nosort" i18n:translate="heading_changedby">Last Action</th> <td>
<th class="nosort" i18n:translate="heading_action">Action</th> <span class="last-history-entry"
</tr> tal:attributes="data-href string:$item_url/@@historyview">
</thead> last history entry
<tbody> </span>
<tal:block repeat="item batch"> </td>
<tr class="commentrow" </tr>
tal:define="even repeat/item/even; </tal:block>
email python:getattr(item.getObject(), 'author_email'); </tbody>
item_url item/getURL;" </table>
tal:attributes="class python: even and 'odd' or 'even'"> <div metal:use-macro="here/batch_macros/macros/navigation" />
<td> </fieldset>
<span tal:content="python:item.author_name or item.Creator">Name</span> </form>
<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"/>
<a href=""
tal:attributes="href string:$item_url/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>
<span class="last-history-entry"
tal:attributes="data-href string:$item_url/@@historyview">
last history entry
</span>
</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>
</tal:block>
</tbody>
</table>
<div metal:use-macro="here/batch_macros/macros/navigation" />
</fieldset>
</form>
</div>
</fieldset>
</div>
</tal:main-macro> </tal:main-macro>

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', '')
IStatusMessage(self.context.REQUEST).addStatusMessage(
_('Comment approved.'), # context.translate() does not know a default for untranslated msgids
type='info') comment_state_translated = \
else: self.context.translate("comment_"+review_state_new)
IStatusMessage(self.context.REQUEST).addStatusMessage( if comment_state_translated == "comment_"+review_state_new:
_('Comment already approved.'), comment_state_translated = review_state_new
type='info')
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 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>