-
Notifications
You must be signed in to change notification settings - Fork 470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue 11265 create 4 eyes workflow actionlet #13366
Changes from 4 commits
956a889
f28f2d5
683267e
1df865f
64047fe
feda0d8
d314a04
f9e34a3
6b81895
1639d11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package com.dotmarketing.portlets.workflows.actionlet; | ||
|
||
import static com.dotmarketing.portlets.workflows.util.WorkflowActionletUtil.getApproversFromHistory; | ||
import static com.dotmarketing.portlets.workflows.util.WorkflowActionletUtil.getParameterValue; | ||
import static com.dotmarketing.portlets.workflows.util.WorkflowActionletUtil.getUsersFromIds; | ||
|
||
import com.dotcms.util.ConversionUtils; | ||
import com.dotmarketing.business.APILocator; | ||
import com.dotmarketing.exception.DotDataException; | ||
import com.dotmarketing.portlets.workflows.model.MultiUserReferenceParameter; | ||
import com.dotmarketing.portlets.workflows.model.WorkflowActionClassParameter; | ||
import com.dotmarketing.portlets.workflows.model.WorkflowActionletParameter; | ||
import com.dotmarketing.portlets.workflows.model.WorkflowHistory; | ||
import com.dotmarketing.portlets.workflows.model.WorkflowProcessor; | ||
import com.dotmarketing.portlets.workflows.util.WorkflowEmailUtil; | ||
import com.dotmarketing.util.Logger; | ||
import com.liferay.portal.model.User; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
/** | ||
* Sometimes, customers would like content to be published if a specific number of people approve | ||
* their content. They are not particular as to which users, they just need a specific number of | ||
* users to approve it before the content goes live. This actionlet enforces what is called the | ||
* '4-Eyes' principle. | ||
* <p> | ||
* This workflow actionlet allows a user to specify the user IDs, email addresses, or role keys | ||
* (i.e., all the users assigned to those roles) which will be in charge of approving the new | ||
* content. If the users that approve the content are greater or equal to the specified number of | ||
* minimum approvers, then the content will move on to the next actionlet in the workflow. | ||
* Otherwise, an email will be sent to all users that haven't approved the content yet. | ||
* | ||
* @author Jose Castro | ||
* @version 4.3.0 | ||
* @since Jan 3, 2018 | ||
*/ | ||
public class FourEyeApproverActionlet extends WorkFlowActionlet { | ||
|
||
private static final long serialVersionUID = 1177885642438262884L; | ||
|
||
private static final String ACTIONLET_NAME = "'4 Eye' Approval"; | ||
private static final String HOW_TO = | ||
"This actionlet implements the '4 Eyes' principle for verifying new content. It takes a comma-separated list of user IDs, email addresses, or role keys " | ||
+ "(i.e., the users assigned to those roles) which will be in charge of reviewing the content, and a minimum number of approvers. " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
+ "If the number of approvers is greater or equal than the specified value, the next sub-action will be executed."; | ||
private static final String USER_ID_DELIMITER = ","; | ||
private static final String PARAM_CONTENT_APPROVERS = "approvers"; | ||
private static final String PARAM_MINIMUM_CONTENT_APPROVERS = "minimumApprovers"; | ||
private static final String PARAM_EMAIL_SUBJECT = "emailSubject"; | ||
private static final String PARAM_EMAIL_BODY = "emailBody"; | ||
private static final String PARAM_IS_HTML = "isHtml"; | ||
|
||
private static final int DEFAULT_MINIMUM_CONTENT_APPROVERS = 2; | ||
|
||
private boolean shouldStop = false; | ||
|
||
private static ArrayList<WorkflowActionletParameter> ACTIONLET_PARAMETERS = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
@Override | ||
public synchronized List<WorkflowActionletParameter> getParameters() { | ||
if (null == ACTIONLET_PARAMETERS) { | ||
ACTIONLET_PARAMETERS = new ArrayList<>(); | ||
ACTIONLET_PARAMETERS | ||
.add(new MultiUserReferenceParameter(PARAM_CONTENT_APPROVERS, | ||
"User IDs, Emails, or Role Keys", null, | ||
true)); | ||
ACTIONLET_PARAMETERS | ||
.add(new WorkflowActionletParameter(PARAM_MINIMUM_CONTENT_APPROVERS, | ||
"Number of Approvers", | ||
String.valueOf(DEFAULT_MINIMUM_CONTENT_APPROVERS), true)); | ||
ACTIONLET_PARAMETERS | ||
.add(new WorkflowActionletParameter(PARAM_EMAIL_SUBJECT, | ||
"Email Subject", | ||
"'4 Eye' Approval Required", true)); | ||
ACTIONLET_PARAMETERS | ||
.add(new WorkflowActionletParameter(PARAM_EMAIL_BODY, "Email Message", | ||
null, | ||
false)); | ||
} | ||
return ACTIONLET_PARAMETERS; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why is this syncr, I would create a static code to init it and return the static value every time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jdotcms I think the intention was to do lazy init. But what you said makes sense to me. Also the ACTIONLET_PARAMETERS field should final, at least that what the name in uppercase implies (constant) |
||
} | ||
|
||
@Override | ||
public String getName() { | ||
return ACTIONLET_NAME; | ||
} | ||
|
||
@Override | ||
public String getHowTo() { | ||
return HOW_TO; | ||
} | ||
|
||
@Override | ||
public boolean stopProcessing() { | ||
return this.shouldStop; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object obj) { | ||
if (null != obj && obj instanceof WorkFlowActionlet) { | ||
return getClass().equals(obj.getClass()); | ||
} | ||
return false; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return (shouldStop ? 1 : 0); | ||
} | ||
|
||
@Override | ||
public void executeAction(final WorkflowProcessor processor, | ||
final Map<String, WorkflowActionClassParameter> params) { | ||
final String userIds = getParameterValue(params.get(PARAM_CONTENT_APPROVERS)); | ||
final int minimumContentApprovers = ConversionUtils | ||
.toInt(getParameterValue(params.get(PARAM_MINIMUM_CONTENT_APPROVERS)), | ||
DEFAULT_MINIMUM_CONTENT_APPROVERS); | ||
final String emailSubject = getParameterValue(params.get(PARAM_EMAIL_SUBJECT)); | ||
final String emailBody = getParameterValue(params.get(PARAM_EMAIL_BODY)); | ||
final boolean isHtml = getParameterValue(params.get(PARAM_IS_HTML), false); | ||
final Set<User> requiredContentApprovers = getUsersFromIds(userIds, USER_ID_DELIMITER); | ||
// Add this approval to the history | ||
final WorkflowHistory history = new WorkflowHistory(); | ||
history.setActionId(processor.getAction().getId()); | ||
history.setMadeBy(processor.getUser().getUserId()); | ||
List<WorkflowHistory> historyList = processor.getHistory(); | ||
if (null == historyList) { | ||
historyList = new ArrayList<>(); | ||
} | ||
historyList.add(history); | ||
final Set<User> hasApproved = getApproversFromHistory(historyList, requiredContentApprovers, | ||
processor.getAction().getId(), minimumContentApprovers); | ||
if (hasApproved.size() < minimumContentApprovers) { | ||
this.shouldStop = true; | ||
// Keep the workflow process on the same step | ||
processor.setNextStep(processor.getStep()); | ||
// Send email to users who have NOT approved only | ||
final List<String> emails = new ArrayList<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be extracted to a private method sendMailsToNotApprovingUsers(). it would include this for loop and the sending part below from lines 159-162 |
||
for (final User user : requiredContentApprovers) { | ||
if (!hasApproved.contains(user)) { | ||
emails.add(user.getEmailAddress()); | ||
} | ||
} | ||
// Assign the workflow step for next assignee | ||
for (final User user : requiredContentApprovers) { | ||
if (!hasApproved.contains(user)) { | ||
try { | ||
processor.setNextAssign(APILocator.getRoleAPI().getUserRole(user)); | ||
break; | ||
} catch (DotDataException e) { | ||
Logger.error(this, | ||
"An error occurred when reassigning workflow step to user '" + user | ||
.getUserId() + "': " + e.getMessage(), e); | ||
} | ||
} | ||
} | ||
final String[] emailsToSend = emails.toArray(new String[emails.size()]); | ||
processor.setWorkflowMessage(emailSubject); | ||
WorkflowEmailUtil | ||
.sendWorkflowEmail(processor, emailsToSend, emailSubject, emailBody, isHtml); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package com.dotmarketing.portlets.workflows.model; | ||
|
||
import com.dotmarketing.business.APILocator; | ||
import com.dotmarketing.business.Role; | ||
import com.dotmarketing.util.Logger; | ||
import com.dotmarketing.util.UtilMethods; | ||
import com.liferay.util.Validator; | ||
import java.util.StringTokenizer; | ||
|
||
/** | ||
* This type of workflow actionlet parameter allows content managers to enter a comma-separated list | ||
* of values used to specify different system users. This class provides 3 mechanisms to specify | ||
* users in you actionlet: | ||
* <ol> | ||
* <li>The user ID.</li> | ||
* <li>The user's email address.</li> | ||
* <li>The role key (this includes all the users associated to the specified role).</li> | ||
* </ol> | ||
* | ||
* @author Jose Castro | ||
* @version 4.3.0 | ||
* @since Jan 9, 2018 | ||
*/ | ||
public class MultiUserReferenceParameter extends WorkflowActionletParameter { | ||
|
||
/** | ||
* Creates a new instance of this class. | ||
* | ||
* @param key The unique key for this parameter. | ||
* @param displayName The human-readable name of this parameter. | ||
* @param defaultValue The optional default value of this parameter. | ||
* @param isRequired If {@code true}, the value of this parameter is required. Otherwise, set | ||
* to {@code false}. | ||
*/ | ||
public MultiUserReferenceParameter(String key, String displayName, | ||
String defaultValue, boolean isRequired) { | ||
super(key, displayName, defaultValue, isRequired); | ||
} | ||
|
||
@Override | ||
public String hasError(final String stringToValidate) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
final StringBuffer errorMsg = new StringBuffer(); | ||
if (UtilMethods.isSet(stringToValidate)) { | ||
final StringTokenizer tokenizer = new StringTokenizer(stringToValidate, ","); | ||
while (tokenizer.hasMoreTokens()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
final String token = tokenizer.nextToken().trim(); | ||
if (Validator.isEmailAddress(token)) { | ||
try { | ||
APILocator.getUserAPI() | ||
.loadByUserByEmail(token, APILocator.getUserAPI().getSystemUser(), | ||
false); | ||
} catch (Exception e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
Logger.error(this.getClass(), "Unable to find user with email: " + token); | ||
errorMsg.append("Unable to find user with email: " + token + "</br>"); | ||
} | ||
} else { | ||
String error = null; | ||
if (isUserId(token)) { | ||
continue; | ||
} else { | ||
error = "Unable to find user with ID: " + token + "</br>"; | ||
if (isRoleKey(token)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
continue; | ||
} else { | ||
error = (UtilMethods.isSet(error)) ? error | ||
: "Unable to find users assigned to role key: " + token; | ||
} | ||
errorMsg.append(error); | ||
} | ||
} | ||
} | ||
} | ||
return (UtilMethods.isSet(errorMsg.toString())) ? errorMsg.toString() : null; | ||
} | ||
|
||
/** | ||
* Checks whether the specified value belongs to an existing user ID or not. | ||
* | ||
* @param userId The potential user ID. | ||
* | ||
* @return Returns {@code true} if the specified value is a valid user ID. Otherwise, returns | ||
* {@code false}. | ||
*/ | ||
private boolean isUserId(final String userId) { | ||
try { | ||
APILocator.getUserAPI() | ||
.loadUserById(userId, APILocator.getUserAPI().getSystemUser(), false); | ||
return Boolean.TRUE; | ||
} catch (Exception e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return Boolean.FALSE; | ||
} | ||
} | ||
|
||
/** | ||
* Checks whether the specified value belongs to an existing role key or not. | ||
* | ||
* @param roleKey The potential role key. | ||
* | ||
* @return Returns {@code true} if the specified value is a valid role key. Otherwise, returns | ||
* {@code false}. | ||
*/ | ||
private boolean isRoleKey(final String roleKey) { | ||
try { | ||
final Role role = APILocator.getRoleAPI().loadRoleByKey(roleKey); | ||
APILocator.getRoleAPI() | ||
.findUsersForRole(role); | ||
return Boolean.TRUE; | ||
} catch (Exception e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return Boolean.FALSE; | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.