API Name: global.WorkflowApprovalUtils

/**
* Workflow approval support utilities
*/


/* global gs, current, GlideRecord, GlideDBObjectManager, Workflow, GlideUser, GlideGroup, GlideController */
/* eslint-disable no-trailing-spaces, curly, strict, no-multi-spaces */

var WorkflowApprovalUtils = Class.create();
WorkflowApprovalUtils.prototype = {

PENDING_STATES: [ 'not requested', 'not_required', 'requested' ],

initialize: function() {
this.debugFlag = gs.getProperty('glide.workflow_approval_utils.log') == 'true';
},

/**
* this tests whether or not a table is a Task table. If it is a task table then
* still set the sysapproval column that references the task table. In doing this
* we will preserve all the sc_XXX ui_macros for now.
*/

isTask: function(tableName) {
var base = '' + GlideDBObjectManager.get().getAbsoluteBase0(tableName);
var task = ( base != null && base == 'task' ) ? true : false;
return task;
},

/**
* Cancel all approvals for a task
*/

cancelAll: function(/*GlideRecord*/ task, /*optional*/ comment) {
this.setAllApprovalsByTask(task, "cancelled", comment);
},

/**
* Set all User approvals for a target record (task or non-task) to the specified state.
*/

setAllApprovalsByTask: function(/*GlideRecord*/ target, approvalState, /*optional*/ comment) {
this.setGroupApprovalsByTask(target.sys_id, approvalState, comment, []);
this.setUserApprovalsByTask(target.sys_id, approvalState, comment, []);
},

/**
* Reset all approvals by deleting all of them along with any
* workflow associated with the task so that the workflows get
* rerun and recalculated.
*/

reset: function(/*GlideRecord*/ task, /*optional*/ comment) {
// delete users first so that business do not run on cascade deletes of user approvals from group approval deletes
gr = new GlideRecord('sysapproval_approver');
gr.setWorkflow(false);
var qc = gr.addQuery('sysapproval', task.sys_id);
qc.addOrCondition('document_id', task.sys_id);
gr.deleteMultiple();

var gr = new GlideRecord('sysapproval_group');
gr.setWorkflow(false);
gr.addQuery('parent', task.sys_id);
gr.deleteMultiple();

new Workflow().deleteWorkflow(task);
if (comment)
this.addApprovalHistoryGR(task, comment);
else
this.addApprovalHistoryGR(task, gs.getMessage("Approvals have been reset"));
},

/**
* Restart workflows (this is deprecated and only exists to support legacy
* calls to this method - use new Workflow().restartWorkflow(task, true)
* instead)
*/

deleteWorkflows: function(/*GlideRecord*/ task, /*optional*/ comment) {
new Workflow().restartWorkflow(task, true);
if (comment)
this.addApprovalHistoryGR(task, comment);
},

/**
* Create user approvals for a sysapproval_group approval
*/

createUserApprovals: function(groupApproval) {
if (groupApproval.assignment_group.nil())
return;

var ids = this.getMembersOfGroup(groupApproval.assignment_group);
var state = groupApproval.approval + '';
var taskId = groupApproval.parent + '';
var approvalId = groupApproval.sys_id + '';

for (var i = 0; i < ids.length; i++) {
var approval = new GlideRecord('sysapproval_approver');
approval.initialize();
approval.sysapproval = taskId;
// fill out reference to task with the in-memory GlideRecord.
// When "Approval Events (Task)" runs it will be able to obtain the task type even though it doesn't yet exist in DB
var target = groupApproval.parent.getRefRecord();
approval.sysapproval.setRefRecord(target);
approval.source_table = target.getRecordClassName();
approval.document_id = target.sys_id +'';
approval.group = approvalId;
approval.approver = ids[i];
approval.wf_activity = groupApproval.wf_activity;
approval.state = state;
if (state == 'approved')
approval.setValue('comments', gs.getMessage('Automatic approval'));

approval.expected_start = groupApproval.expected_start;
approval.due_date = groupApproval.due_date;
approval.insert();
}
},

/**
* Get the list of user sys_ids for each member of a group
*/

getMembersOfGroup: function(groupID) {
var ids = [];
if (!groupID)
return ids;

var gr = new GlideRecord('sys_user_grmember');
gr.addQuery('group', groupID);
gr.addNotNullQuery('user');
gr.addQuery('user.active', true);
gr.query();
while (gr.next()) {
ids.push(gr.getValue('user'));
}

return ids;
},

/**
* Get an object of group approvals that were created by all of the children of
* an approval manager activity (we use this to determine if we are about to
* create a duplicate and to determine if we are reusing an approval that
* we previously created)
*
* We return an object that has:
* key -> sys_id for the group approval
* value -> { sys_id: sys_id of the approval record
* state: current approval state
* group_id: sys_id of the group on the approval record
* activity_id: sys_id of the activity that created the approval record
* }
*/

getGroupApprovalsByApprovalManager: function(taskId, amId) {
var activityIds = this._getActivitiesForAM(amId);
if (activityIds.length == 0)
return {};

return this._buildApprovalObject(false, taskId, activityIds);
},

/**
* Get an object of user approvals that were created by all of the children of
* an approval manager activity (we use this to determine if we are about to
* create a duplicate and to determine if we are reusing an approval that
* we previously created)
*
* We return an object that has:
* key -> sys_id for the user approval
* value -> { sys_id: sys_id of the approval record
* state: current approval state
* user_id: sys_id of the user on the approval record
* activity_id: sys_id of the activity that created the approval record
* }
*/

getUserApprovalsByApprovalManager: function(taskId, amId) {
var activityIds = this._getActivitiesForAM(amId);
if (activityIds.length == 0)
return {};

return this._buildApprovalObject(true, taskId, activityIds);
},

/**
* create a approval object based on given approval type.
* if it has activity attached, then activityIds are used to filter out approval records.
*/

_buildApprovalObject: function(isUserApproval, taskId, activityIds) {
var ids = {};
var approvalType = isUserApproval ? 'sysapproval_approver' : 'sysapproval_group';

var gr = new GlideRecord(approvalType);
gr.orderBy('sys_created_on','ASC');
gr.addQuery('wf_activity', activityIds);
if (isUserApproval)
gr.addQuery('sysapproval', taskId);
else {
gr.addQuery('parent', taskId);
gr.addActiveQuery();
}

gr.query();
while (gr.next()) {
var approver_id = null;
var state = null;
if (isUserApproval) {
approver_id = gr.approver.toString();
state = gr.state.toString();
}
else {
approver_id = gr.assignment_group.toString();
state = gr.approval.toString();
}

ids[gr.sys_id.toString()] = {
sys_id: gr.sys_id.toString(),
state: state,
approver_id: approver_id,
activity_id: gr.wf_activity.toString()
};
}
return ids;
},

_getActivitiesForAM: function(amId) {
// get the list of activities associated with this activity manager
var activityIds = [];
var gr = new GlideRecord('wf_activity');
gr.addQuery('parent', amId);
gr.query();
while (gr.next())
activityIds.push(gr.sys_id.toString());

return activityIds;
},

/**
* Get the counts for the user approvals of a task
*/

getUserApprovalCounts: function(taskID) {
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('sysapproval', taskID);
gr.query();
return this._getApprovalCounts(gr, 'state', 'approver');
},

/**
* Get the counts for the user approvals of a group approval
*/

getUserGroupApprovalCounts: function(groupID) {
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('group', groupID);
gr.query();
return this._getApprovalCounts(gr, 'state', 'approver');
},

/**
* Get the counts for the user approvals of a group approval
* by the approval ids
*/

getGroupUserApprovalCounts: function(approvalIDs) {
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('sys_id', approvalIDs);
gr.query();
return this._getApprovalCounts(gr, 'state', 'approver');
},

/**
* Get the counts for the group approvals of a task
*/

getGroupApprovalCounts: function(taskID) {
var gr = new GlideRecord('sysapproval_group');
gr.addQuery('parent', taskID);
gr.query();
return this._getApprovalCounts(gr, 'approval', 'assignment_group');
},

/**
* Get the counts for the list of user approvals
*
* ids is an array of user approval sys_ids
*/

getUserIdListApprovalCounts: function(ids) {
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('sys_id', ids);
gr.query();
return this._getApprovalCounts(gr, 'state', 'approver');
},

/**
* Get the counts for the list of group approvals
*
* ids is an array of group approval sys_ids
*/

getGroupIdListApprovalCounts: function(ids) {
var gr = new GlideRecord('sysapproval_group');
gr.addQuery('sys_id', ids);
gr.query();
return this._getApprovalCounts(gr, 'approval', 'assignment_group');
},

/**
* Return an object with counts and (optionally) user ids of the
* approval states of the passed in records
*
* The returned object is:
*
* obj.counts{key = state, value = count}
* .approvalIDs{key = state, value = array of ids}
*/

_getApprovalCounts: function(gr, approvalField, idField) {
var counts = {
total : 0,
not_requested : 0,
requested : 0,
approved : 0,
rejected : 0,
cancelled : 0,
not_required : 0,
duplicate: 0
};
var approvalIDs = {
not_requested: [],
requested: [],
approved: [],
rejected: [],
cancelled: [],
not_required: [],
duplicate: []
};

while (gr.next()) {
var state = gr.getValue(approvalField);
state = state.replace(/\s/g, "_");
if (!counts[state])
counts[state] = 0;

counts[state] = counts[state] + 1;
counts.total++;

if (idField) {
if (!approvalIDs[state])
approvalIDs[state] = [];

approvalIDs[state].push(gr.getValue(idField));
}
}

if (this.debugFlag) {
var debugString = "Approvals for: " + gr.getDisplayValue();
for (var s in counts)
debugString += " " + s + "=" + counts[s];
this.debug(debugString);
}

var ret = {};
ret.counts = counts;
ret.approvalIDs = approvalIDs;

return ret;
},

getUserApprovalIdsForGroups: function(groups) {
var app_ids = {};
for (var idx in groups) {
var group = groups[idx]+'';

if (typeof app_ids[group] === "undefined")
app_ids[group] = [];
var apps = new GlideRecord('sysapproval_approver');
apps.addQuery('group', group);
apps.query();

while(apps.next())
app_ids[group].push(apps.sys_id+'');

}
return app_ids;
},

/**
* Set all group approvals of a parent task to the specified state
*/

setGroupApprovalsByTask: function(taskID, approvalState, /*optional*/ comment, /*optional []*/ currentStates) {
var gr = new GlideRecord('sysapproval_group');
gr.addQuery('parent', taskID);
if (currentStates && currentStates.length > 0)
gr.addQuery('approval', currentStates);

gr.query();
this._setGroupApprovals(gr, approvalState, comment);
},

/**
* Set all group approvals of a parent task to the specified state
*/

setPendingGroupApprovalsByTask: function(taskID, approvalState, /*optional*/ comment) {
var gr = new GlideRecord('sysapproval_group');
gr.addQuery('parent', taskID);
gr.addQuery('approval', this.PENDING_STATES);
gr.query();
this._setGroupApprovals(gr, approvalState, comment);
},

/**
* Set all specified group approvals to the specified state
*/

setGroupApprovalsByIds: function(ids, approvalState, /*optional*/ comment, /*optional []*/ currentStates) {
var gr = new GlideRecord('sysapproval_group');
gr.addQuery('sys_id', ids);
if (currentStates && currentStates.length > 0)
gr.addQuery('approval', currentStates);

gr.query();
this._setGroupApprovals(gr, approvalState, comment);
},

/**
* Set specified pending group approvals to the specified state
*/

setPendingGroupApprovalsByIds: function(ids, approvalState, /*optional*/ comment) {
if (!ids || ids.length == 0)
return;

var gr = new GlideRecord('sysapproval_group');
gr.addQuery('sys_id', ids);
gr.addQuery('approval', this.PENDING_STATES);
gr.query();
this._setGroupApprovals(gr, approvalState, comment);
},

/**
* Set all User approvals of a task to the specified state
* - modified to look at document_id for all non-task tables
*/

setUserApprovalsByTask: function(taskID, approvalState, /*optional*/ comment, /*optional []*/ currentStates) {
var gr = new GlideRecord('sysapproval_approver');
var gr1 = gr.addQuery('sysapproval', taskID);
gr1.addOrCondition('document_id', taskID);

if (currentStates && currentStates.length > 0)
gr.addQuery('state', currentStates);

gr.query();
this._setUserApprovals(gr, approvalState, comment);
},

/**
* Set all pending user approvals of a task to the specified state
* - modified to look for document_id when approving a non-task table
*/

setPendingUserApprovalsByTask: function(taskID, approvalState, /*optional*/ comment) {
var gr = new GlideRecord('sysapproval_approver');

if( this.isTask( current.getRecordClassName() ) )
gr.addQuery('sysapproval', taskID);
else
gr.addQuery('document_id', taskID);
gr.addQuery('state', this.PENDING_STATES);
gr.query();
this._setUserApprovals(gr, approvalState, comment);
},

/**
* Set all user approvals of a group approval to the specified state
*/

setUserApprovalsByGroup: function(groupID, approvalState, /*optional*/ comment, /*optional []*/ currentStates) {
if (!groupID || GlideStringUtil.nil(groupID))
return;

var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('group', groupID);
if (currentStates && currentStates.length > 0)
gr.addQuery('state', currentStates);

gr.query();
this._setUserApprovals(gr, approvalState, comment);
},

/**
* Set all pending user approvals of a group approval to the specified state
*/

setPendingUserApprovalsByGroup: function(groupID, approvalState, /*optional*/ comment) {
if (!groupID)
return;

var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('group', groupID);
gr.addQuery('state', this.PENDING_STATES);
gr.query();
this._setUserApprovals(gr, approvalState, comment, true);
},

/**
* Set all specified user approvals to the specified state
*/

setUserApprovalsByIds: function(ids, approvalState, /*optional*/ comment, /*optional []*/ currentStates) {
if (!ids || ids.length == 0)
return;

var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('sys_id', ids);
if (currentStates && currentStates.length > 0)
gr.addQuery('state', currentStates);

gr.query();
this._setUserApprovals(gr, approvalState, comment);
},

/**
* Set specified pending user approvals to the specified state
*/

setPendingUserApprovalsByIds: function(ids, approvalState, /*optional*/ comment) {
if (!ids || ids.length == 0)
return;

var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('sys_id', ids);
gr.addQuery('state', this.PENDING_STATES);
gr.query();
this._setUserApprovals(gr, approvalState, comment);
},

/**
* Set group approvals to a specific state and, optionally, add an
* approval history comment
*/

_setGroupApprovals: function(gr, approvalState, /*optional*/ comment) {
while (gr.next()) {
if (gr.approval != approvalState) {
gr.setValue('approval', approvalState);
if (comment)
this.addApprovalHistoryGR(gr, comment);

// If we are coming from a Reset Approvals with a state of Cancelled
// we do not want to progress the workflow further
if (approvalState === 'cancelled') {
gr.setValue("active", false);
gr.setValue("state", "4");
}

gr.update();
}
}
},

/**
* Set user approvals to a specific state and, optionally, add a comment
*
*/

_setUserApprovals: function(gr, approvalState, /*optional*/ comment, /*optional*/ syncDatesFlag) {
while (gr.next()) {
// If this is running in an Approval Coordinator with Duplicates Deleted
// This approval may still be required
if (approvalState == 'not_required' && this._runningAMApproval(gr))
continue;

if (gr.state != approvalState) {
gr.setValue('state', approvalState);

if (comment)
gr.comments = comment;

if (syncDatesFlag && gr.group) {
gr.expected_start = gr.group.expected_start;
gr.due_date = gr.group.due_date;
}
gr.update();
}
}
},

/**
* Check for another Executing activity that is relying on this user approval
*
*/

_runningAMApproval: function(approvalGR) {
var taskID = new GlideStringUtil().notNil(approvalGR.sysapproval) ? approvalGR.sysapproval : approvalGR.document_id;
// Get all executing activities for this context
var executing = new GlideRecord('wf_executing');
executing.addQuery('context.id', taskID);
executing.addQuery('activity', '!=', approvalGR.wf_activity);
executing.addQuery('state', 'waiting');
executing.addQuery('scratchpad', 'CONTAINS', approvalGR.sys_id);
executing.query();
return executing.hasNext();
},

/**
* Add a list of ids to the approval list
*/

addIdsToApprovalList: function(approvals, o) {
if (!o)
return;

if (typeof o == 'string') {
o = o.split(',');
}

for (var i = 0; i < o.length; i++) {
var ids = this._checkFieldNameInId(o[i]);
if (!ids)
continue;

ids = ids.split(',');
for(var j=0; j < ids.length; j++) {
approvals[ids[j]] = true;
}
}
},

/**
* Add a list of user or group ids to the appropriate approval list
*/

addUsersAndGroupsToApprovalList: function(users, groups, o) {
if (!o)
return;

if (typeof o == 'string')
o = o.split(',');

for (var i = 0; i < o.length; i++) {
// handle ${name}
var id = this._checkFieldNameInId(o[i]);
if (!id)
continue;

id = id.toString().split(',');
for(var j=0; j < id.length; j++) {
// determine if this is a user or a group
if (this._isValidUser(id[j]))
users[id[j]] = true;
else if (this._isValidGroup(id[j]))
groups[id[j]] = true;
}
}
},

/**
* Handle ${field_name} replacement
*/

_checkFieldNameInId: function(id) {
if (!id)
return "";

if ((id.indexOf("${") == 0) && (id.substring(id.length - 1) == "}")) {
id = this._evaluateScript("current." + id.substring(2, id.length - 1));
}
return id;
},

_isValidUser: function(id) {
if (GlideStringUtil.nil(id))
return false;
var usr = new GlideRecord("sys_user");
var query = usr.addQuery("sys_id", id);
query.addOrCondition("user_name", id);
usr.queryNoDomain();
return usr.hasNext();
},

_isValidGroup: function(id) {
if (GlideStringUtil.nil(id))
return false;
var ugr = new GlideRecord("sys_user_group");
var query = ugr.addQuery("sys_id", id);
query.addOrCondition("name", id);
ugr.queryNoDomain();
return ugr.hasNext();
},

/**
* Evaluate the script for this group approval rule, returning
* the group id or group list
*/

_evaluateScript: function(script) {
if (!script)
return '';

return GlideController.evaluateString(script);
},

/**
* Create the group approvals from the group list
*/

createGroupApprovers: function(groups, order, state) {
if (!state)
state = 'not requested';

var gr = new GlideRecord('sysapproval_group');
for (var id in groups) {
gr.initialize();
gr.short_description = current.short_description;
gr.parent = current.sys_id;
gr.assignment_group = id;
gr.approval = state;
gr.addDomainQuery(current);
gr.insert();
}
},

/**
* Add an approval history journal entry for a task record
*/

addApprovalHistoryGR: function(taskGR, msg) {
taskGR.setDisplayValue('approval_history', msg);
},

/**
* Add an approval history journal entry for a task id (read the task record,
* add the journal entry and update the task record)
*/

addApprovalHistoryID: function(taskID, msg) {
var gr = new GlideRecord('task');
if (gr.get(taskID)) {
gr.setDisplayValue('approval_history', msg);
gr.update();
}
},

/**
* Get the user name from a user sys_id
*/

getUserName: function(userSysId) {
if (!userSysId)
return '';

var userInfo = GlideUser.getUserByID(userSysId);
if (!userInfo)
return '';

return userInfo.getDisplayName() + '';
},


/**
* Get approval record for a given task/record id
*/

getApprovalRecord: function(taskID) {
var apRecords = new GlideRecord('sysapproval_approver');
if (GlideStringUtil.nil(taskID))
return apRecords;

var task = this.isTask(current.getRecordClassName());
if (task) {
apRecords.addQuery('sysapproval', taskID);
apRecords.query();
}

if (apRecords.hasNext())
return apRecords;

apRecords.addQuery('document_id', taskID);
apRecords.query();
return apRecords;
},

getRequestedApproversAsString: function(taskID) {
var apRecords = new GlideRecord('sysapproval_approver');
apRecords.addQuery('state', 'requested');

var task = this.isTask(current.getRecordClassName());
if(task)
apRecords.addQuery('sysapproval', taskID);
else
apRecords.addQuery('document_id', taskID);
apRecords.query();

var list = '';
var comma = false;
while (apRecords.next()) {
var id = apRecords.getValue('approver');
var name = this.resolveUserIdToName(id);
if (comma)
list+= ', ';
list+= name;
comma = true;
}

list = list.length > 0 ? ' ' + gs.getMessage('by') + ' ' + list : list;

return list;
},

resolveUserIdToName: function (userID) {
var userGR = new GlideRecord('sys_user');
var name = '';
if (userGR.get(userID)) {
name+= userGR.first_name + ' ' + userGR.last_name;
}

return name;
},

debug: function(msg) {
if (this.debugFlag)
gs.print("WorkflowApprovalUtils: " + msg);
},

/**
* Get state of all specified user approvals
*/

getUserApprovalStateById: function(ids) {
if (!ids || ids.length == 0)
return;

var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('sys_id', ids);
gr.query();
if (gr.next())
return gr.getValue('state');

return;
},

/**
* Get state of all specified user approvals
*/

getGroupApprovalStateById: function(ids) {
if (!ids || ids.length == 0)
return;

var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('group', ids);
gr.query();
while(gr.next()) {
if (gr.getValue('state') != 'not requested')
return gr.getValue('state');
}
return;
},

unapprove: function() {
var id = current.sysapproval.nil() ? current.document_id : current.getValue('sysapproval');
var table = current.source_table.nil() ? 'task' : current.source_table;
if (id != null && table != null ) {
var gr = new GlideRecord(table);
if (gr.get(id)) {
new Workflow().runFlows(gr, 'update');
}
}
},

approve: function() {
var id = current.sysapproval.nil() ? current.document_id : current.getValue('sysapproval');
var table = current.source_table.nil() ? 'task' : current.source_table;
if (id != null && table != null ) {
var gr = new GlideRecord(table);
if (gr.get(id)) {
new Workflow().broadcastEventToCurrentsContexts(gr, 'update', null);
}
}
},

type: 'WorkflowApprovalUtils'
};