bulk moderation of comments

extended for comment_multiple_state_review_workflow, refactoring and tests
This commit is contained in:
Katja Suess
2019-12-08 20:01:40 +01:00
parent d93525ff27
commit bf20752b69
11 changed files with 341 additions and 193 deletions
@@ -100,6 +100,14 @@
permission="plone.app.discussion.ReviewComments"
/>
<browser:page
for="*"
name="translationhelper"
layer="..interfaces.IDiscussionLayer"
class=".moderation.TranslationHelper"
permission="plone.app.discussion.ReviewComments"
/>
<!-- Comments viewlet -->
<browser:viewlet
@@ -1,6 +1,6 @@
/******************************************************************************
*
* jQuery functions for the plone.app.discussion bulk moderation.
* jQuery functions for the plone.app.discussion moderation.
*
******************************************************************************/
@@ -84,19 +84,80 @@ require(["jquery", "pat-registry"], function($, registry) {
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).closest("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') {
// TODO: translate message
alert("You haven't selected a bulk action. Please select one.");
} else if (valArray.length === 0) {
// TODO: 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) {
// reset the bulkaction select
selectField.find("option[value='-1']").attr('selected', 'selected');
// reload filtered comments
$("#review-comments").load(window.location + " #review-comments", function() {
init();
$('.pat-plone-modal').patPloneModal();
});
});
}
});
/**********************************************************************
* 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);
});
});
/**********************************************************************
* Bulk actions for comments (delete, publish)
+11 -6
View File
@@ -15,13 +15,14 @@
<tal:main-macro metal:define-macro="main"
tal:define="toLocalizedTime nocall:context/@@plone/toLocalizedTime;
items view/comments;
filter view/filter|nothing;
filter request/review_state|nothing;
Batch python:modules['Products.CMFPlone'].Batch;
b_size python:30;
b_start python:0;
b_start request/b_start | b_start;
moderation_enabled view/moderation_enabled;
colorclass python:lambda x: 'state-private' if x=='rejected' else ('state-internal' if x=='spam' else 'state-'+x);
translationhelper nocall:context/@@translationhelper;
">
<script type="text/javascript"
tal:attributes="src string:${context/portal_url}/++plone++plone.app.discussion.javascripts/moderation.js">
@@ -33,7 +34,7 @@
<div class="portalMessage warning"
role="status"
tal:condition="not: view/moderation_enabled">
tal:condition="not: moderation_enabled">
<strong i18n:translate="">Warning</strong>
<span tal:omit-tag="" i18n:translate="message_moderation_disabled">
Moderation workflow is disabled. You have to
@@ -56,6 +57,7 @@
<form method="post"
action="#"
tal:condition="moderation_enabled"
tal:attributes="action string:${context/absolute_url}/@@bulk-actions"
tal:define="batch python:Batch(items, b_size, int(b_start), orphan=1);">
@@ -76,7 +78,7 @@
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>
<label tal:attributes="for review_state"><span tal:content="python:translationhelper.translate_comment_review_state(review_state)">review_state</span></label>
</tal:workflow-filter>
</fieldset>
</th>
@@ -85,12 +87,15 @@
<th id="bulkactions" class="nosort" colspan="7">
<select name="form.select.BulkAction">
<option selected="selected" value="-1" i18n:translate="title_bulkactions">Bulk Actions</option>
<tal:comment tal:replace="nothing"></tal:comment>
<option value="publish"
i18n:translate="bulkactions_publish"
tal:condition="python: filter != 'published' and moderation_enabled">Approve</option>
tal:condition="python: filter != 'published'">Approve</option>
<option value="mark_as_spam"
tal:condition="python: filter != 'spam'">Spam</option>
<option value="delete" i18n:translate="bulkactions_delete">Delete</option>
</select>
<input type="hidden" name="form.button.Filter" tal:attributes="value filter" value="" />
<input type="hidden" name="filter" tal:attributes="value filter"/>
<input id="dobulkaction"
type="submit"
class="standalone allowMultiSubmit"
@@ -184,7 +189,7 @@
value="Label"
tal:attributes="id item/id;
data-transition transition/id;
value python:view.translate(transition['title'])"
value python:translationhelper.translate(transition['title'])"
/>
</tal:transitions>
</div>
+47 -49
View File
@@ -4,6 +4,7 @@ from AccessControl import Unauthorized
from Acquisition import aq_inner
from Acquisition import aq_parent
from plone.app.discussion.events import CommentPublishedEvent
from plone.app.discussion.events import CommentTransitionEvent
from plone.app.discussion.events import CommentDeletedEvent
from plone.app.discussion.interfaces import _
from plone.app.discussion.interfaces import IComment
@@ -31,6 +32,15 @@ _('Reject')
_('Spam')
class TranslationHelper(BrowserView):
def translate(self, text=""):
return _(text)
def translate_comment_review_state(self, rs):
return _("comment_" + rs, default=rs)
class View(BrowserView):
"""Show comment moderation view."""
@@ -42,9 +52,9 @@ class View(BrowserView):
pass
def __init__(self, context, request):
self.context = context
self.request = request
super(View, self).__init__(context, request)
self.workflowTool = getToolByName(self.context, 'portal_workflow')
self.transitions = []
def __call__(self):
self.request.set('disable_border', True)
@@ -74,11 +84,10 @@ class View(BrowserView):
A 'review workflow' is characterized by implementing a 'pending'
workflow state.
"""
comment_workflow = self.workflowTool.getChainForPortalType(
workflows = self.workflowTool.getChainForPortalType(
'Discussion Item')
if comment_workflow:
comment_workflow = comment_workflow[0]
comment_workflow = self.workflowTool[comment_workflow]
if workflows:
comment_workflow = self.workflowTool[workflows[0]]
if 'pending' in comment_workflow.states:
return True
return False
@@ -91,17 +100,16 @@ class View(BrowserView):
A 'review multipe state workflow' is characterized by implementing
a 'rejected' workflow state and a 'spam' workflow state.
"""
comment_workflow = self.workflowTool.getChainForPortalType(
workflows = self.workflowTool.getChainForPortalType(
'Discussion Item')
if comment_workflow:
comment_workflow = comment_workflow[0]
comment_workflow = self.workflowTool[comment_workflow]
if 'rejected' in comment_workflow.states:
if workflows:
comment_workflow = self.workflowTool[workflows[0]]
if 'spam' in comment_workflow.states:
return True
return False
def allowed_transitions(self, obj=None):
"""Return allowed workflow transitions.
"""Return allowed workflow transitions for obj.
Example: pending
@@ -123,8 +131,6 @@ class View(BrowserView):
]
return transitions
def translate(self, text=""):
return _(text)
class ModerateCommentsEnabled(BrowserView):
@@ -265,13 +271,11 @@ class CommentTransition(BrowserView):
comment.reindexObject()
content_object.reindexObject(idxs=['total_comments'])
notify(CommentPublishedEvent(self.context, comment))
# for complexer workflows:
notify(CommentTransitionEvent(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
comment_state_translated = self.context.restrictedTraverse("translationhelper").translate_comment_review_state(review_state_new)
msgid = _(
"comment_transmitted",
@@ -327,7 +331,7 @@ class RejectComment(BrowserView):
class BulkActionsView(BrowserView):
"""Bulk actions (approve, delete, reject, recall, mark as spam).
"""Call bulk action: publish/approve, delete (, reject, recall, mark as spam).
Each table row of the moderation view has a checkbox with the absolute
path (without host and port) of the comment objects:
@@ -349,6 +353,10 @@ class BulkActionsView(BrowserView):
"""
def __init__(self, context, request):
super(BulkActionsView, self).__init__(context, request)
self.workflowTool = getToolByName(context, 'portal_workflow')
def __call__(self):
"""Call BulkActionsView."""
if 'form.select.BulkAction' in self.request:
@@ -358,50 +366,40 @@ class BulkActionsView(BrowserView):
if bulkaction == '-1':
# no bulk action was selected
pass
elif bulkaction == 'retract':
self.retract()
elif bulkaction == 'publish':
self.publish()
elif bulkaction == 'mark_as_spam':
self.mark_as_spam()
elif bulkaction == 'delete':
self.delete()
else:
raise KeyError # pragma: no cover
self.transmit(bulkaction)
def retract(self):
raise NotImplementedError
def transmit(self, action=None):
"""Transmit all comments in the paths variable to requested review_state.
def publish(self):
"""Publishes all comments in the paths variable.
Expects a list of absolute paths (without host and port):
/Plone/startseite/++conversation++default/1286200010610352
Expects a list of absolute paths (without host and port):
/Plone/startseite/++conversation++default/1286200010610352
"""
context = aq_inner(self.context)
for path in self.paths:
comment = context.restrictedTraverse(path)
content_object = aq_parent(aq_parent(comment))
workflowTool = getToolByName(comment, 'portal_workflow')
current_state = workflowTool.getInfoFor(comment, 'review_state')
if current_state != 'published':
workflowTool.doActionFor(comment, 'publish')
comment.reindexObject()
content_object.reindexObject(idxs=['total_comments'])
notify(CommentPublishedEvent(content_object, comment))
def mark_as_spam(self):
raise NotImplementedError
allowed_transitions = [
transition['id'] for transition in self.workflowTool.listActionInfos(object=comment)
if transition['category'] == 'workflow' and transition['allowed']
]
if action in allowed_transitions:
self.workflowTool.doActionFor(comment, action)
comment.reindexObject()
content_object.reindexObject(idxs=['total_comments'])
notify(CommentPublishedEvent(content_object, comment))
# for complexer workflows:
notify(CommentTransitionEvent(self.context, comment))
def delete(self):
"""Deletes all comments in the paths variable.
"""Delete all comments in the paths variable.
Expects a list of absolute paths (without host and port):
/Plone/startseite/++conversation++default/1286200010610352
Expects a list of absolute paths (without host and port):
/Plone/startseite/++conversation++default/1286200010610352
"""
context = aq_inner(self.context)
for path in self.paths: