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"
|
||||
/>
|
||||
|
||||
<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:
|
||||
|
@ -6,6 +6,7 @@ from plone.app.discussion.interfaces import ICommentRemovedEvent
|
||||
from plone.app.discussion.interfaces import IDiscussionEvent
|
||||
from plone.app.discussion.interfaces import ICommentDeletedEvent
|
||||
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 IReplyRemovedEvent
|
||||
from zope.interface import implementer
|
||||
@ -62,3 +63,8 @@ class CommentDeletedEvent(DiscussionEvent):
|
||||
class CommentPublishedEvent(DiscussionEvent):
|
||||
""" 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):
|
||||
""" 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-map>
|
||||
</state>
|
||||
<state state_id="published" title="Published">
|
||||
<state state_id="published" title="Approved">
|
||||
<description>Visible to everyone, non-editable.</description>
|
||||
<exit-transition transition_id="mark_as_spam"/>
|
||||
<exit-transition transition_id="recall"/>
|
||||
@ -76,7 +76,7 @@
|
||||
</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>
|
||||
<action url="%(content_url)s/content_status_modify?workflow_action=recall" category="workflow" icon="">Recall</action>
|
||||
<guard>
|
||||
<guard-permission>Review comments</guard-permission>
|
||||
</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())
|
||||
|
||||
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):
|
||||
self.request.set('form.select.BulkAction', 'publish')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||
@ -135,15 +127,6 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
# 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)
|
||||
|
||||
self.assertRaises(NotImplementedError,
|
||||
view)
|
||||
|
||||
def test_delete(self):
|
||||
# Initially we have three comments
|
||||
self.assertEqual(len(self.conversation.objectIds()), 3)
|
||||
|
Loading…
Reference in New Issue
Block a user