Name

global.ChangeRequestStateHandlerSNC

Description

Base state handler implementation extended by ChangeRequestStateHandler. Used by ChangeTypeChgReqAPI.

Script

var ChangeRequestStateHandlerSNC = Class.create();

ChangeRequestStateHandlerSNC.LOG_PROPERTY = "com.snc.change_management.core.log";

ChangeRequestStateHandlerSNC.DRAFT = "draft";
ChangeRequestStateHandlerSNC.ASSESS = "assess";
ChangeRequestStateHandlerSNC.AUTHORIZE = "authorize";
ChangeRequestStateHandlerSNC.SCHEDULED = "scheduled";
ChangeRequestStateHandlerSNC.IMPLEMENT = "implement";
ChangeRequestStateHandlerSNC.REVIEW = "review";
ChangeRequestStateHandlerSNC.CLOSED = "closed";
ChangeRequestStateHandlerSNC.CANCELED = "canceled";

ChangeRequestStateHandlerSNC.CHG_TYPE_MODEL_CACHE = "com.snc.change_management.type_model_cache";

ChangeRequestStateHandlerSNC.prototype = {
  /**
   * Keep a mapping of the state values and their label equivalent to make the models (ChangeRequestStateModel_normal...etc)
   * easier to work with
   */
  STATE_NAMES: {
  	"-5": ChangeRequestStateHandlerSNC.DRAFT,
  	"-4": ChangeRequestStateHandlerSNC.ASSESS,
  	"-3": ChangeRequestStateHandlerSNC.AUTHORIZE,
  	"-2": ChangeRequestStateHandlerSNC.SCHEDULED,
  	"-1": ChangeRequestStateHandlerSNC.IMPLEMENT,
  	"0": ChangeRequestStateHandlerSNC.REVIEW,
  	"3": ChangeRequestStateHandlerSNC.CLOSED,
  	"4": ChangeRequestStateHandlerSNC.CANCELED
  },
  
  STATE_VALUES: null,

  NORMAL: "normal",
  EMERGENCY: "emergency",
  STANDARD: "standard",

  DEFAULT_MODEL_CLASS: "ChangeRequestStateModel_normal",

  /**
   * @param changeRequestGr - GlideRecord
   */
  initialize: function(changeRequestGr) {
  	this._initPrivateCacheable(ChangeRequestStateHandlerSNC.CHG_TYPE_MODEL_CACHE);
  	
  	this.log = new GSLog(ChangeRequestStateHandlerSNC.LOG_PROPERTY, this.type);
  	this.log.setLog4J();
  	
  	this._changeUtils = new ChangeUtils();

  	if (!changeRequestGr)
  		return;

  	// If the we're enforcing data requirements, check approvals for change on canMove.
  	this._checkApproval = gs.getProperty(ChangeRequestSNC.ENFORCE_DATA_REQ_PROP) === "true" ? true : false;
  	
  	this._gr = changeRequestGr;
  	this._resetModel();
  },

  // Accepts state values and the 'known names' (above) of legacy states
  isNext: function(state) {
  	state = state+"";
  	var nextStates = this.getNextStates();
  	
  	if (!nextStates)
  		return false;
  	
  	// Always compare values so assume state is a value if there's no name for it
  	state = this.getStateValue(state);
  	
  	var isNextState = false;
  	nextStates.some(function(nextState) {
  		nextState = this.getStateValue(nextState);
  		isNextState = (state === nextState);
  		return isNextState;
  	}, this);

  	return isNextState;
  },

  /**
   * Moves the change request to the next state.
   * 
   * The record's state is set but the record is not saved.
   * 
   * @returns {Boolean}
   */
  next: function() {
  	var nextStates = this.getNextStates();
  	
  	if (!nextStates || nextStates.length == 0)
  		return false;
  	
  	if (this.log.atLevel(GSLog.DEBUG))
  		this.log.debug("[next] trying to move to " + nextStates[0]);

  	var nextState = nextStates[0];
  	if (!this.canMoveTo(nextState))
  		return false;
  	
  	nextState = this.getStateValue(nextState);

  	var currentState = this.getStateName(this._gr.getValue('state'));
  	if (this._model[currentState][nextState])
  		this._model[currentState][nextState].moving.call(this._model);
  	this._gr.state = this.getStateValue(nextStates[0]);
  	return true;
  },

  /**
   * Moves the change request to the given state
   * 
   * The record's state is set but the record is not saved.
   * 
   * @param toState String - name of the state to move to
   * @returns {Boolean}
   */
  moveTo: function(toState) {
  	if (!this.canMoveTo(toState))
  		return false;
  			
  	var currentState = this.getStateName(this._gr.getValue('state'));
  	this._model[currentState][toState].moving.call(this._model);
  	this._gr.state = this.getStateValue(toState);
  	return true;
  },

  /**
   * Confirms that the move was allowed and executes the 'moving' function for this specific transition
   * 
   * @param toState String - name of the state to move to
   * @returns {Boolean}
   */
  moveFrom: function(fromState) {
  	if (!this.canMoveFrom(fromState))
  		return false;
  	
  	var currentState = this.getStateName(this._gr.getValue('state'));
  	if (this._model[fromState] && this._model[fromState][currentState])
  		this._model[fromState][currentState].moving.call(this._model);

  	return true;
  },

  /**
   * Checks if the change request at its current state is allowed to move to the given state
   * 
   * @param toState (optional) Name of the state
   * @returns {Boolean}
   */
  canMoveTo: function(toState) {
  	if (!toState)
  		return false;
  	
  	toState = toState + "";

  	// The state needs to be a legacy name at this point, or a numeric for new states
  	toState = this.STATE_NAMES[toState] || toState;
  	
  	var currentState = this.getStateName(this._gr.getValue('state'));
  	
  	// Legacy type based change
  	// If we're enforcing data requirements, check approvals for all state changes apart from 'to draft' and 'to cancelled'
  	if (currentState !== ChangeRequestStateHandler.DRAFT &&
  		this._checkApproval &&
  		toState !== ChangeRequestStateHandler.DRAFT &&
  		toState !== ChangeRequestStateHandler.CANCELED &&
  		(this._gr.approval + "" === "requested" || this._gr.approval + "" === "rejected"))
  	{
  		this.log.debug("[canMoveTo] Approval is required before state change");
  		return false;
  	}
  	
  	if (!this._model[currentState]) {
  		this.log.debug("[canMoveTo] " + currentState + " is not a valid state for this change");
  		return false;
  	}

  	if (this._model[currentState][toState])
  		return this._model[currentState][toState].canMove.call(this._model);

  	if (this.log.atLevel(GSLog.DEBUG))
  		this.log.debug("[canMoveTo] " + toState + " is not a valid state to move to");

  	return false;
  },

  /**
   * Checks if the change request at its current state was allowed to move from the state provided
   * 
   * @param fromState (optional) Name of the state
   * @returns {Boolean}
   */
  canMoveFrom: function(fromState) {
  	if (!fromState)
  		return false;

  	fromState = fromState + "";

  	// The state needs to be a legacy name at this point, or a numeric for new states
  	fromState = this.STATE_NAMES[fromState] || fromState;
  	
  	// Legacy type based change
  	var currentState = this.getStateName(this._gr.getValue('state'));
  	if (!this._model[currentState]) {
  		this.log.debug("[canMoveFrom] " + currentState + " is not a valid state for this change");
  		return false;
  	}

  	if (!this._model[fromState]) {
  		this.log.debug("[canMoveFrom] " + fromState + " is not a valid state for this change to have moved from");
  		return false;
  	}

  	if (this._model[fromState][currentState])
  		return this._model[fromState][currentState].canMove.call(this._model);

  	this.log.debug("[canMoveFrom] " + currentState + " is not a valid state to move from " + fromState);

  	return false;
  },
  /**
   * Returns the array of next states this change to may move to.
   * 
   * The array will be made up of legacy state names (shown above) if there is a name for the value,
   * and state values to accommodate any new states added via the model
   */
  getNextStates: function() {
  	var currentState = null;
  	//If we're dealing with a change model
  	
  	// Legacy Type based changes
  	currentState = this.getStateName(this._gr.getValue('state'));
  	if (!currentState)
  		return null;

  	var stateObj = this._model[currentState];

  	if (!stateObj) {
  		this.log.debug("[getNextStates] " + currentState + " is not a valid state for this change");
  		return null;
  	}

  	if (!stateObj.nextState || stateObj.nextState.length === 0) {
  		this.log.debug("[getNextStates] there is no 'next' state to move to from '" + currentState + "'");
  		return null;
  	}

  	return stateObj.nextState;
  },

  getAvailableStates: function() {
  	var currentState = null;

  	// Legacy Type based changes
  	currentState = this.getStateName(this._gr.getValue('state'));
  	if (!currentState)
  		return null;

  	var stateObj = this._model[currentState];

  	if (!stateObj) {

  		if (this.log.atLevel(GSLog.DEBUG))
  			this.log.debug("[getAvailableStates] " + currentState + " is not a valid state for this change");

  		return null;
  	}

  	var availableStates = Object.keys(stateObj).filter(function(elem) {
  		return elem.indexOf('nextState') === -1;
  	});

  	if (!availableStates || availableStates.length === 0) {

  		if (this.log.atLevel(GSLog.DEBUG))
  			this.log.debug("[getAvailableStates] there is no 'next' state to move to from '" + currentState + "'");

  		return null;
  	}

  	return availableStates;
  },

  /**
   * Dissociate approvals from a particular workflow activity to preserve the history.
   */
  disassociateApprovalsFromWorkflow: function() {
  	if (!this._gr)
  		return;

  	var existingApprovalsGr = new GlideMultipleUpdate("sysapproval_approver");
  	existingApprovalsGr.addQuery("sysapproval", this._gr.getUniqueValue());
  	existingApprovalsGr.addQuery("state", "!=", "cancelled");
  	existingApprovalsGr.setValue("wf_activity", "");
  	existingApprovalsGr.execute();

  	existingApprovalsGr = new GlideMultipleUpdate("sysapproval_group");
  	existingApprovalsGr.addQuery("parent", this._gr.getUniqueValue());
  	existingApprovalsGr.addQuery("approval", "!=", "cancelled");
  	existingApprovalsGr.setValue("wf_activity", "");
  	existingApprovalsGr.execute();
  },

  /**
   * Uses the current change request's type to determine and set the state model class
   */
  _resetModel: function() {
  	this._model = null;
  	switch (this._gr.getValue('type') + "") {
  		case this.NORMAL:
  			this._model = new ChangeRequestStateModel_normal(this._gr);
  			break;
  		case this.EMERGENCY:
  			this._model = new ChangeRequestStateModel_emergency(this._gr);
  			break;
  		case this.STANDARD:
  			this._model = new ChangeRequestStateModel_standard(this._gr);
  			break;
  		default:
  			this._model = this._deriveModel();
  			break;
  	}
  },
  
  getStateModel: function() {
  	var stateModel = {};
  	stateModel['types'] = [];
  	stateModel['stateValueByName'] = {};
  	stateModel['stateNameByValue'] = {};
  	stateModel['stateLabelByValue'] = {};
  	
  	var states = this._changeUtils.getFieldChoices('state');
  	for (var i = 0; i < states.size(); i++) {
  		var state = states.get(i);
  		var stateValue = state.getValue();
  		stateModel.stateLabelByValue[stateValue] = state.getLabel();
  		var stateName = this.getStateName(stateValue);
  		if (stateName) {
  			stateModel.stateNameByValue[stateValue] = stateName;
  			stateModel.stateValueByName[stateName] = stateValue;
  		}
  	}

  	var chgTypes = this._changeUtils.getFieldChoices('type');
  	if (!chgTypes || chgTypes.size() == 0)
  		return stateModel;
  	
  	for (var i = 0; i < chgTypes.size(); i++) {
  		var chgType = chgTypes.get(i);
  		var chgTypeValue = chgType.getValue();
  		if (!chgTypeValue)
  			continue;
  		stateModel.types.push(chgTypeValue);
  		stateModel[chgTypeValue] = this._getStateModelByType(chgTypeValue);
  	}
  	
  	return stateModel;
  },
  
  _getStateModelByType: function(type) {
  	var typeStateModel = {};
  	
  	var outerScope = JSUtil.getGlobal();
  	var changeRequestStateModel_type = "ChangeRequestStateModel_" + type; 
  	var stateModelClass = outerScope[changeRequestStateModel_type].prototype;
  	if (typeof stateModelClass === "undefined")
  		return typeStateModel;

  	var stateValues = Object.keys(this.STATE_NAMES);
  	for (var i = 0; i < stateValues.length; i++) {
  		var stateName = this.getStateName(stateValues[i]);
  		var stateInClass = stateModelClass[stateName];
  		if (!stateInClass)
  			continue;
  		
  		typeStateModel[stateName] = {nextState: []};
  		if (stateInClass.nextState)
  			typeStateModel[stateName].nextState = stateInClass.nextState;

  		typeStateModel[stateName].availableStates = Object.keys(stateInClass).filter(function(elem) {
  			return elem.indexOf('nextState') === -1;
  		});
  	}
  	
  	return typeStateModel;
  },

  // Return a know state name or the lookup value
  getStateName: function(stateValue) {
  	return this.STATE_NAMES[stateValue + ""] || stateValue;
  },

  //Return a know state value or the lookup value
  getStateValue: function(stateName) {
  	if (this.STATE_VALUES === null)
  		this._initStateValue();
  	return this.STATE_VALUES[stateName + ""] || stateName;
  },

  isOnHold: function() {
  	return this._model.isOnHold.call(this._model);
  },

  _initStateValue: function() {
  	this.STATE_VALUES = {};
  	Object.keys(this.STATE_NAMES).forEach(function(legacyState) {
  		this.STATE_VALUES[this.STATE_NAMES[legacyState]] = legacyState;
  	}, this);
  },

  _initPrivateCacheable: function(name) {
  	if (!name)
  		return;
  	if (GlideCacheManager.get(name, "_created_") !== null)
  		return;
  	GlideCacheManager.addPrivateCacheable(name);
  	GlideCacheManager.put(name, "_created_", new GlideDateTime().getNumericValue());
  },

  _deriveModel: function() {
  	var type = this._gr.getValue("type");
  	if (!type) {
  		this.log.logWarning("_deriveModel: " + this._gr.getDisplayValue() + " type not specified. Using the " + this.DEFAULT_MODEL_CLASS + " script include.");
  		return this._getModel(this.DEFAULT_MODEL_CLASS);
  	}

  	// Already know the script-include required
  	var scriptInclude = GlideCacheManager.get(ChangeRequestStateHandlerSNC.CHG_TYPE_MODEL_CACHE, type);
  	if (scriptInclude instanceof String)
  		return this._getModel(scriptInclude);

  	// Specific naming of models for Change Types is required and outlined in documentation
  	var scriptIncludeName = "ChangeRequestStateModel_" + type;
  	var scriptIncludeGR = new GlideRecord("sys_script_include");
  	scriptIncludeGR.addQuery("name", scriptIncludeName);
  	scriptIncludeGR.addActiveQuery();
  	scriptIncludeGR.setLimit(1);
  	scriptIncludeGR.query();

  	// By default we use the DEFAULT_MODEL_CLASS implementation
  	if (!scriptIncludeGR.next()) {
  		this.log.logWarning("_deriveModel: " + this._gr.getDisplayValue() + " Cannot find the " + scriptIncludeName + " script include for type of " + type + ". Using the " + this.DEFAULT_MODEL_CLASS + " script include.");
  		return this._getModel(this.DEFAULT_MODEL_CLASS);
  	}

  	var apiName = scriptIncludeGR.getValue("api_name");
  	// Still have nothing, just instantiate the default
  	if (!apiName) {
  		this.log.logWarning("_deriveModel: " + this._gr.getDisplayValue() + " Cannot get the API name from the " + scriptIncludeName + " script include. Using the " + this.DEFAULT_MODEL_CLASS + " script include.");
  		return this._getModel(this.DEFAULT_MODEL_CLASS);
  	}

  	var model = this._getModel(apiName);
  	if (typeof model !== "object") {
  		this.log.logWarning("_deriveModel: " + this._gr.getDisplayValue() + " Cannot instantiate the " + apiName + " script include. Using the " + this.DEFAULT_MODEL_CLASS + " script include.");
  		return this._getModel(this.DEFAULT_MODEL_CLASS);
  	}

  	// Cache the corresponding script-include for this type
  	GlideCacheManager.put(ChangeRequestStateHandlerSNC.CHG_TYPE_MODEL_CACHE, type, apiName);

  	return model;
  },

  _getModel: function (scriptInclude) {
  	var modelClass = new GlideScriptEvaluator().evaluateString(scriptInclude, true);
  	return new modelClass(this._gr);
  },

  type: "ChangeRequestStateHandlerSNC"
};

Sys ID

3112be37cb100200d71cb9c0c24c9c2a

Offical Documentation

Official Docs: