bulk moderation of comments
extended for comment_multiple_state_review_workflow, refactoring and tests
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user