bulk moderation of comments
extended for comment_multiple_state_review_workflow, refactoring and tests
This commit is contained in:
parent
d93525ff27
commit
bf20752b69
@ -100,6 +100,14 @@
|
|||||||
permission="plone.app.discussion.ReviewComments"
|
permission="plone.app.discussion.ReviewComments"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<browser:page
|
||||||
|
for="*"
|
||||||
|
name="translationhelper"
|
||||||
|
layer="..interfaces.IDiscussionLayer"
|
||||||
|
class=".moderation.TranslationHelper"
|
||||||
|
permission="plone.app.discussion.ReviewComments"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<!-- Comments viewlet -->
|
<!-- Comments viewlet -->
|
||||||
<browser: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();
|
init();
|
||||||
$(".pat-plone-modal").patPloneModal();
|
$(".pat-plone-modal").patPloneModal();
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
},
|
/**********************************************************************
|
||||||
error: function(msg) {
|
* Bulk actions for comments (delete, publish)
|
||||||
alert(
|
**********************************************************************/
|
||||||
"Error transmitting comment. (Error sending AJAX request:" +
|
$("input[name='form.button.BulkAction']").click(function (e) {
|
||||||
target +
|
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)
|
* Bulk actions for comments (delete, publish)
|
||||||
|
@ -15,13 +15,14 @@
|
|||||||
<tal:main-macro metal:define-macro="main"
|
<tal:main-macro metal:define-macro="main"
|
||||||
tal:define="toLocalizedTime nocall:context/@@plone/toLocalizedTime;
|
tal:define="toLocalizedTime nocall:context/@@plone/toLocalizedTime;
|
||||||
items view/comments;
|
items view/comments;
|
||||||
filter view/filter|nothing;
|
filter request/review_state|nothing;
|
||||||
Batch python:modules['Products.CMFPlone'].Batch;
|
Batch python:modules['Products.CMFPlone'].Batch;
|
||||||
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);
|
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"
|
<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">
|
||||||
@ -33,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="portalMessage warning"
|
<div class="portalMessage warning"
|
||||||
role="status"
|
role="status"
|
||||||
tal:condition="not: view/moderation_enabled">
|
tal:condition="not: moderation_enabled">
|
||||||
<strong i18n:translate="">Warning</strong>
|
<strong i18n:translate="">Warning</strong>
|
||||||
<span tal:omit-tag="" i18n:translate="message_moderation_disabled">
|
<span tal:omit-tag="" i18n:translate="message_moderation_disabled">
|
||||||
Moderation workflow is disabled. You have to
|
Moderation workflow is disabled. You have to
|
||||||
@ -56,6 +57,7 @@
|
|||||||
|
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="#"
|
action="#"
|
||||||
|
tal:condition="moderation_enabled"
|
||||||
tal:attributes="action string:${context/absolute_url}/@@bulk-actions"
|
tal:attributes="action string:${context/absolute_url}/@@bulk-actions"
|
||||||
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);">
|
||||||
|
|
||||||
@ -76,7 +78,7 @@
|
|||||||
value review_state;
|
value review_state;
|
||||||
id review_state;
|
id review_state;
|
||||||
checked python:request.review_state==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>
|
</tal:workflow-filter>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</th>
|
</th>
|
||||||
@ -85,12 +87,15 @@
|
|||||||
<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>
|
||||||
|
<tal:comment tal:replace="nothing"></tal:comment>
|
||||||
<option value="publish"
|
<option value="publish"
|
||||||
i18n:translate="bulkactions_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>
|
<option value="delete" i18n:translate="bulkactions_delete">Delete</option>
|
||||||
</select>
|
</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"
|
<input id="dobulkaction"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="standalone allowMultiSubmit"
|
class="standalone allowMultiSubmit"
|
||||||
@ -184,7 +189,7 @@
|
|||||||
value="Label"
|
value="Label"
|
||||||
tal:attributes="id item/id;
|
tal:attributes="id item/id;
|
||||||
data-transition transition/id;
|
data-transition transition/id;
|
||||||
value python:view.translate(transition['title'])"
|
value python:translationhelper.translate(transition['title'])"
|
||||||
/>
|
/>
|
||||||
</tal:transitions>
|
</tal:transitions>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@ from AccessControl import Unauthorized
|
|||||||
from Acquisition import aq_inner
|
from Acquisition import aq_inner
|
||||||
from Acquisition import aq_parent
|
from Acquisition import aq_parent
|
||||||
from plone.app.discussion.events import CommentPublishedEvent
|
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.events import CommentDeletedEvent
|
||||||
from plone.app.discussion.interfaces import _
|
from plone.app.discussion.interfaces import _
|
||||||
from plone.app.discussion.interfaces import IComment
|
from plone.app.discussion.interfaces import IComment
|
||||||
@ -31,6 +32,15 @@ _('Reject')
|
|||||||
_('Spam')
|
_('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):
|
class View(BrowserView):
|
||||||
"""Show comment moderation view."""
|
"""Show comment moderation view."""
|
||||||
|
|
||||||
@ -42,9 +52,9 @@ class View(BrowserView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, context, request):
|
def __init__(self, context, request):
|
||||||
self.context = context
|
super(View, self).__init__(context, request)
|
||||||
self.request = request
|
|
||||||
self.workflowTool = getToolByName(self.context, 'portal_workflow')
|
self.workflowTool = getToolByName(self.context, 'portal_workflow')
|
||||||
|
self.transitions = []
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
self.request.set('disable_border', True)
|
self.request.set('disable_border', True)
|
||||||
@ -74,11 +84,10 @@ class View(BrowserView):
|
|||||||
A 'review workflow' is characterized by implementing a 'pending'
|
A 'review workflow' is characterized by implementing a 'pending'
|
||||||
workflow state.
|
workflow state.
|
||||||
"""
|
"""
|
||||||
comment_workflow = self.workflowTool.getChainForPortalType(
|
workflows = self.workflowTool.getChainForPortalType(
|
||||||
'Discussion Item')
|
'Discussion Item')
|
||||||
if comment_workflow:
|
if workflows:
|
||||||
comment_workflow = comment_workflow[0]
|
comment_workflow = self.workflowTool[workflows[0]]
|
||||||
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
|
||||||
@ -91,17 +100,16 @@ class View(BrowserView):
|
|||||||
A 'review multipe state workflow' is characterized by implementing
|
A 'review multipe state workflow' is characterized by implementing
|
||||||
a 'rejected' workflow state and a 'spam' workflow state.
|
a 'rejected' workflow state and a 'spam' workflow state.
|
||||||
"""
|
"""
|
||||||
comment_workflow = self.workflowTool.getChainForPortalType(
|
workflows = self.workflowTool.getChainForPortalType(
|
||||||
'Discussion Item')
|
'Discussion Item')
|
||||||
if comment_workflow:
|
if workflows:
|
||||||
comment_workflow = comment_workflow[0]
|
comment_workflow = self.workflowTool[workflows[0]]
|
||||||
comment_workflow = self.workflowTool[comment_workflow]
|
if 'spam' in comment_workflow.states:
|
||||||
if 'rejected' in comment_workflow.states:
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def allowed_transitions(self, obj=None):
|
def allowed_transitions(self, obj=None):
|
||||||
"""Return allowed workflow transitions.
|
"""Return allowed workflow transitions for obj.
|
||||||
|
|
||||||
Example: pending
|
Example: pending
|
||||||
|
|
||||||
@ -123,8 +131,6 @@ class View(BrowserView):
|
|||||||
]
|
]
|
||||||
return transitions
|
return transitions
|
||||||
|
|
||||||
def translate(self, text=""):
|
|
||||||
return _(text)
|
|
||||||
|
|
||||||
|
|
||||||
class ModerateCommentsEnabled(BrowserView):
|
class ModerateCommentsEnabled(BrowserView):
|
||||||
@ -265,13 +271,11 @@ class CommentTransition(BrowserView):
|
|||||||
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))
|
||||||
|
# for complexer workflows:
|
||||||
|
notify(CommentTransitionEvent(self.context, comment))
|
||||||
review_state_new = workflowTool.getInfoFor(comment, 'review_state', '')
|
review_state_new = workflowTool.getInfoFor(comment, 'review_state', '')
|
||||||
|
|
||||||
# context.translate() does not know a default for untranslated msgids
|
comment_state_translated = self.context.restrictedTraverse("translationhelper").translate_comment_review_state(review_state_new)
|
||||||
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 = _(
|
msgid = _(
|
||||||
"comment_transmitted",
|
"comment_transmitted",
|
||||||
@ -327,7 +331,7 @@ class RejectComment(BrowserView):
|
|||||||
|
|
||||||
|
|
||||||
class BulkActionsView(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
|
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:
|
||||||
@ -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):
|
def __call__(self):
|
||||||
"""Call BulkActionsView."""
|
"""Call BulkActionsView."""
|
||||||
if 'form.select.BulkAction' in self.request:
|
if 'form.select.BulkAction' in self.request:
|
||||||
@ -358,50 +366,40 @@ class BulkActionsView(BrowserView):
|
|||||||
if bulkaction == '-1':
|
if bulkaction == '-1':
|
||||||
# no bulk action was selected
|
# no bulk action was selected
|
||||||
pass
|
pass
|
||||||
elif bulkaction == 'retract':
|
|
||||||
self.retract()
|
|
||||||
elif bulkaction == 'publish':
|
|
||||||
self.publish()
|
|
||||||
elif bulkaction == 'mark_as_spam':
|
|
||||||
self.mark_as_spam()
|
|
||||||
elif bulkaction == 'delete':
|
elif bulkaction == 'delete':
|
||||||
self.delete()
|
self.delete()
|
||||||
else:
|
else:
|
||||||
raise KeyError # pragma: no cover
|
self.transmit(bulkaction)
|
||||||
|
|
||||||
def retract(self):
|
def transmit(self, action=None):
|
||||||
raise NotImplementedError
|
"""Transmit all comments in the paths variable to requested review_state.
|
||||||
|
|
||||||
def publish(self):
|
Expects a list of absolute paths (without host and port):
|
||||||
"""Publishes all comments in the paths variable.
|
|
||||||
|
|
||||||
Expects a list of absolute paths (without host and port):
|
|
||||||
|
|
||||||
/Plone/startseite/++conversation++default/1286200010610352
|
|
||||||
|
|
||||||
|
/Plone/startseite/++conversation++default/1286200010610352
|
||||||
"""
|
"""
|
||||||
context = aq_inner(self.context)
|
context = aq_inner(self.context)
|
||||||
for path in self.paths:
|
for path in self.paths:
|
||||||
comment = context.restrictedTraverse(path)
|
comment = context.restrictedTraverse(path)
|
||||||
content_object = aq_parent(aq_parent(comment))
|
content_object = aq_parent(aq_parent(comment))
|
||||||
workflowTool = getToolByName(comment, 'portal_workflow')
|
allowed_transitions = [
|
||||||
current_state = workflowTool.getInfoFor(comment, 'review_state')
|
transition['id'] for transition in self.workflowTool.listActionInfos(object=comment)
|
||||||
if current_state != 'published':
|
if transition['category'] == 'workflow' and transition['allowed']
|
||||||
workflowTool.doActionFor(comment, 'publish')
|
]
|
||||||
comment.reindexObject()
|
if action in allowed_transitions:
|
||||||
content_object.reindexObject(idxs=['total_comments'])
|
self.workflowTool.doActionFor(comment, action)
|
||||||
notify(CommentPublishedEvent(content_object, comment))
|
comment.reindexObject()
|
||||||
|
content_object.reindexObject(idxs=['total_comments'])
|
||||||
def mark_as_spam(self):
|
notify(CommentPublishedEvent(content_object, comment))
|
||||||
raise NotImplementedError
|
# for complexer workflows:
|
||||||
|
notify(CommentTransitionEvent(self.context, comment))
|
||||||
|
|
||||||
def delete(self):
|
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):
|
Expects a list of absolute paths (without host and port):
|
||||||
|
|
||||||
/Plone/startseite/++conversation++default/1286200010610352
|
|
||||||
|
|
||||||
|
/Plone/startseite/++conversation++default/1286200010610352
|
||||||
"""
|
"""
|
||||||
context = aq_inner(self.context)
|
context = aq_inner(self.context)
|
||||||
for path in self.paths:
|
for path in self.paths:
|
||||||
|
@ -6,6 +6,7 @@ from plone.app.discussion.interfaces import ICommentRemovedEvent
|
|||||||
from plone.app.discussion.interfaces import IDiscussionEvent
|
from plone.app.discussion.interfaces import IDiscussionEvent
|
||||||
from plone.app.discussion.interfaces import ICommentDeletedEvent
|
from plone.app.discussion.interfaces import ICommentDeletedEvent
|
||||||
from plone.app.discussion.interfaces import ICommentPublishedEvent
|
from plone.app.discussion.interfaces import ICommentPublishedEvent
|
||||||
|
from plone.app.discussion.interfaces import ICommentTransitionEvent
|
||||||
from plone.app.discussion.interfaces import IReplyAddedEvent
|
from plone.app.discussion.interfaces import IReplyAddedEvent
|
||||||
from plone.app.discussion.interfaces import IReplyRemovedEvent
|
from plone.app.discussion.interfaces import IReplyRemovedEvent
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
@ -62,3 +63,8 @@ class CommentDeletedEvent(DiscussionEvent):
|
|||||||
class CommentPublishedEvent(DiscussionEvent):
|
class CommentPublishedEvent(DiscussionEvent):
|
||||||
""" Event to be triggered when a Comment is publicated
|
""" Event to be triggered when a Comment is publicated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(ICommentTransitionEvent)
|
||||||
|
class CommentTransitionEvent(DiscussionEvent):
|
||||||
|
"""Event to be triggered when a Comments review_state changed."""
|
||||||
|
@ -422,3 +422,7 @@ class ICommentPublishedEvent(IDiscussionEvent):
|
|||||||
class ICommentDeletedEvent(IDiscussionEvent):
|
class ICommentDeletedEvent(IDiscussionEvent):
|
||||||
""" Notify user on comment delete
|
""" Notify user on comment delete
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ICommentTransitionEvent(IDiscussionEvent):
|
||||||
|
"""Notify user on comment transition / change of review_state."""
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dc-workflow workflow_id="comment_3state_review_workflow" title="Comment Review Workflow" description="A simple review workflow for comments" state_variable="review_state" initial_state="pending" manager_bypass="False">
|
|
||||||
<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="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="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="">
|
|
||||||
<exit-transition transition_id="publish"/>
|
|
||||||
</state>
|
|
||||||
<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="reject" title="Reject" new_state="rejected" trigger="USER" before_script="" after_script="">
|
|
||||||
<action url="%(content_url)s/content_status_modify?workflow_action=rejected" 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>
|
|
@ -36,7 +36,7 @@
|
|||||||
<permission-role>Reviewer</permission-role>
|
<permission-role>Reviewer</permission-role>
|
||||||
</permission-map>
|
</permission-map>
|
||||||
</state>
|
</state>
|
||||||
<state state_id="published" title="Published">
|
<state state_id="published" title="Approved">
|
||||||
<description>Visible to everyone, non-editable.</description>
|
<description>Visible to everyone, non-editable.</description>
|
||||||
<exit-transition transition_id="mark_as_spam"/>
|
<exit-transition transition_id="mark_as_spam"/>
|
||||||
<exit-transition transition_id="recall"/>
|
<exit-transition transition_id="recall"/>
|
||||||
@ -76,7 +76,7 @@
|
|||||||
</guard>
|
</guard>
|
||||||
</transition>
|
</transition>
|
||||||
<transition transition_id="recall" title="Reviewer recalls comment back to pending state" new_state="pending" trigger="USER" before_script="" after_script="">
|
<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>
|
<action url="%(content_url)s/content_status_modify?workflow_action=recall" category="workflow" icon="">Recall</action>
|
||||||
<guard>
|
<guard>
|
||||||
<guard-permission>Review comments</guard-permission>
|
<guard-permission>Review comments</guard-permission>
|
||||||
</guard>
|
</guard>
|
||||||
|
64
plone/app/discussion/tests/robot/test_moderation.robot
Normal file
64
plone/app/discussion/tests/robot/test_moderation.robot
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
*** Settings ***
|
||||||
|
|
||||||
|
Resource plone/app/robotframework/saucelabs.robot
|
||||||
|
Resource plone/app/robotframework/selenium.robot
|
||||||
|
|
||||||
|
Library Remote ${PLONE_URL}/RobotRemote
|
||||||
|
|
||||||
|
Test Setup Run Keywords Plone test setup
|
||||||
|
Test Teardown Run keywords Plone test teardown
|
||||||
|
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
|
||||||
|
Add a Comment to a Document and bulk delete it
|
||||||
|
Given a logged-in Site Administrator
|
||||||
|
and workflow multiple enabled
|
||||||
|
and a document with discussion enabled
|
||||||
|
When I add a comment and delete it
|
||||||
|
Then I can not see the comment below the document
|
||||||
|
|
||||||
|
|
||||||
|
*** Keywords ***
|
||||||
|
|
||||||
|
# Given
|
||||||
|
|
||||||
|
a logged-in Site Administrator
|
||||||
|
Enable autologin as Site Administrator
|
||||||
|
|
||||||
|
a document
|
||||||
|
Create content type=Document id=my-document title=My Document
|
||||||
|
|
||||||
|
a document with discussion enabled
|
||||||
|
a document
|
||||||
|
I enable discussion on the document
|
||||||
|
|
||||||
|
|
||||||
|
# When
|
||||||
|
|
||||||
|
I enable discussion on the document
|
||||||
|
Go To ${PLONE_URL}/my-document/edit
|
||||||
|
Wait until page contains Settings
|
||||||
|
Click Link Settings
|
||||||
|
Wait until element is visible name=form.widgets.IAllowDiscussion.allow_discussion:list
|
||||||
|
Select From List name=form.widgets.IAllowDiscussion.allow_discussion:list True
|
||||||
|
Click Button Save
|
||||||
|
|
||||||
|
I add a comment and delete it
|
||||||
|
Wait until page contains element id=form-widgets-comment-text
|
||||||
|
Input Text id=form-widgets-comment-text This is a comment
|
||||||
|
Click Button Comment
|
||||||
|
Go To ${PLONE_URL}/@@moderate-comments?review_state=all
|
||||||
|
Select from list by value xpath://select[@name='form.select.BulkAction'] delete
|
||||||
|
Select Checkbox name=check_all
|
||||||
|
Click Button Apply
|
||||||
|
|
||||||
|
workflow multiple enabled
|
||||||
|
Go To ${PLONE_URL}/@@content-controlpanel?type_id=Discussion%20Item&new_workflow=comment_multiple_state_review_workflow
|
||||||
|
Click Button Save
|
||||||
|
|
||||||
|
# Then
|
||||||
|
|
||||||
|
I can not see the comment below the document
|
||||||
|
Go To ${PLONE_URL}/my-document/view
|
||||||
|
Page should not contain This is a comment
|
@ -0,0 +1,124 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from plone.app.discussion.browser.moderation import BulkActionsView
|
||||||
|
from plone.app.discussion.browser.moderation import DeleteComment
|
||||||
|
from plone.app.discussion.browser.moderation import CommentTransition
|
||||||
|
from plone.app.discussion.browser.moderation import View
|
||||||
|
from plone.app.discussion.interfaces import IConversation
|
||||||
|
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||||
|
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||||
|
from plone.app.testing import setRoles
|
||||||
|
from plone.app.testing import TEST_USER_ID
|
||||||
|
from plone.registry.interfaces import IRegistry
|
||||||
|
from Products.CMFCore.utils import getToolByName
|
||||||
|
from zope.component import createObject
|
||||||
|
from zope.component import queryUtility
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||||
|
|
||||||
|
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = self.layer['app']
|
||||||
|
self.portal = self.layer['portal']
|
||||||
|
self.request = self.layer['request']
|
||||||
|
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||||
|
self.wf = getToolByName(self.portal,
|
||||||
|
'portal_workflow',
|
||||||
|
None)
|
||||||
|
self.context = self.portal
|
||||||
|
self.portal.portal_workflow.setChainForPortalTypes(
|
||||||
|
('Discussion Item',),
|
||||||
|
'comment_multiple_state_review_workflow',
|
||||||
|
)
|
||||||
|
self.wf_tool = self.portal.portal_workflow
|
||||||
|
# Add a conversation with three comments
|
||||||
|
conversation = IConversation(self.portal.doc1)
|
||||||
|
comment1 = createObject('plone.Comment')
|
||||||
|
comment1.title = 'Comment 1'
|
||||||
|
comment1.text = 'Comment text'
|
||||||
|
comment1.Creator = 'Jim'
|
||||||
|
new_id_1 = conversation.addComment(comment1)
|
||||||
|
self.comment1 = self.portal.doc1.restrictedTraverse(
|
||||||
|
'++conversation++default/{0}'.format(new_id_1),
|
||||||
|
)
|
||||||
|
comment2 = createObject('plone.Comment')
|
||||||
|
comment2.title = 'Comment 2'
|
||||||
|
comment2.text = 'Comment text'
|
||||||
|
comment2.Creator = 'Joe'
|
||||||
|
new_id_2 = conversation.addComment(comment2)
|
||||||
|
self.comment2 = self.portal.doc1.restrictedTraverse(
|
||||||
|
'++conversation++default/{0}'.format(new_id_2),
|
||||||
|
)
|
||||||
|
comment3 = createObject('plone.Comment')
|
||||||
|
comment3.title = 'Comment 3'
|
||||||
|
comment3.text = 'Comment text'
|
||||||
|
comment3.Creator = 'Emma'
|
||||||
|
new_id_3 = conversation.addComment(comment3)
|
||||||
|
self.comment3 = self.portal.doc1.restrictedTraverse(
|
||||||
|
'++conversation++default/{0}'.format(new_id_3),
|
||||||
|
)
|
||||||
|
self.conversation = conversation
|
||||||
|
|
||||||
|
def test_default_bulkaction(self):
|
||||||
|
# Make sure no error is raised when no bulk actions has been supplied
|
||||||
|
self.request.set('form.select.BulkAction', '-1')
|
||||||
|
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||||
|
|
||||||
|
view = BulkActionsView(self.portal, self.request)
|
||||||
|
|
||||||
|
self.assertFalse(view())
|
||||||
|
|
||||||
|
def test_publish(self):
|
||||||
|
self.request.set('form.select.BulkAction', 'publish')
|
||||||
|
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||||
|
view = BulkActionsView(self.portal, self.request)
|
||||||
|
|
||||||
|
view()
|
||||||
|
|
||||||
|
# Count published comments
|
||||||
|
published_comments = 0
|
||||||
|
for r in self.conversation.getThreads():
|
||||||
|
comment_obj = r['comment']
|
||||||
|
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
|
||||||
|
if workflow_status == 'published':
|
||||||
|
published_comments += 1
|
||||||
|
# Make sure the comment has been published
|
||||||
|
self.assertEqual(published_comments, 1)
|
||||||
|
|
||||||
|
def test_mark_as_spam(self):
|
||||||
|
self.request.set('form.select.BulkAction', 'mark_as_spam')
|
||||||
|
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||||
|
|
||||||
|
view = BulkActionsView(self.portal, self.request)
|
||||||
|
|
||||||
|
view()
|
||||||
|
|
||||||
|
# Count spam comments
|
||||||
|
spam_comments = 0
|
||||||
|
for r in self.conversation.getThreads():
|
||||||
|
comment_obj = r['comment']
|
||||||
|
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
|
||||||
|
if workflow_status == 'spam':
|
||||||
|
spam_comments += 1
|
||||||
|
# Make sure the comment has been marked as spam
|
||||||
|
self.assertEqual(spam_comments, 1)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
# Initially we have three comments
|
||||||
|
self.assertEqual(len(self.conversation.objectIds()), 3)
|
||||||
|
# Delete two comments with bulk actions
|
||||||
|
self.request.set('form.select.BulkAction', 'delete')
|
||||||
|
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()),
|
||||||
|
'/'.join(self.comment3.getPhysicalPath())])
|
||||||
|
view = BulkActionsView(self.app, self.request)
|
||||||
|
|
||||||
|
view()
|
||||||
|
|
||||||
|
# Make sure that the two comments have been deleted
|
||||||
|
self.assertEqual(len(self.conversation.objectIds()), 1)
|
||||||
|
comment = next(self.conversation.getComments())
|
||||||
|
self.assertTrue(comment)
|
||||||
|
self.assertEqual(comment, self.comment2)
|
@ -110,14 +110,6 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(view())
|
self.assertFalse(view())
|
||||||
|
|
||||||
def test_retract(self):
|
|
||||||
self.request.set('form.select.BulkAction', 'retract')
|
|
||||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
|
||||||
|
|
||||||
view = BulkActionsView(self.portal, self.request)
|
|
||||||
|
|
||||||
self.assertRaises(NotImplementedError, view)
|
|
||||||
|
|
||||||
def test_publish(self):
|
def test_publish(self):
|
||||||
self.request.set('form.select.BulkAction', 'publish')
|
self.request.set('form.select.BulkAction', 'publish')
|
||||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||||
@ -135,15 +127,6 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
|||||||
# Make sure the comment has been published
|
# Make sure the comment has been published
|
||||||
self.assertEqual(published_comments, 1)
|
self.assertEqual(published_comments, 1)
|
||||||
|
|
||||||
def test_mark_as_spam(self):
|
|
||||||
self.request.set('form.select.BulkAction', 'mark_as_spam')
|
|
||||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
|
||||||
|
|
||||||
view = BulkActionsView(self.portal, self.request)
|
|
||||||
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
view)
|
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
# Initially we have three comments
|
# Initially we have three comments
|
||||||
self.assertEqual(len(self.conversation.objectIds()), 3)
|
self.assertEqual(len(self.conversation.objectIds()), 3)
|
||||||
|
Loading…
Reference in New Issue
Block a user