Periodic Review, Part X

“A person with a clear purpose will make progress on even the roughest road. A person with no purpose will make no progress on even the smoothest road.”
Thomas Carlyle

Last time, we brought our process far enough along to send out some empty notices, and now we need to create the content for those notices that will inform the recipient of the actions required of them. Before we jump into that, though, we still need to add a little bit more code to our Script Include to wrap the process and update the execution record with the results. At the end of our sendNotices function, let’s add the following code:

// finalize the execution
var itemLabel = 'item';
if (executionGR.total_items > 1) {
	itemLabel = 'items';
}
var noticeLabel = 'notice was';
if (noticeCt > 1) {
	noticeLabel = 'notices were';
}
executionGR.total_notices = noticeCt;
executionGR.run_end = new GlideDateTime();
executionGR.state = 'Complete';
executionGR.completion_code = 0;
executionGR.description = noticeCt + ' ' + noticeLabel + ' generated and sent out covering a total of ' + executionGR.total_items + ' ' + itemLabel + '.';
executionGR.update();

This will update the state of the execution and provide some statistics and a description of the execution. We also need to do one more thing to link the notice records to their corresponding email records, but before we can do that, we have to give the event time to fire and the process time to react to the event firing. We can use a gs.sleep command to do that, but since this is a scoped application, we will have to use a little workaround to get things to work.

// update the references to the sent email
var sleeper = new global.Sleeper();
sleeper.sleep(10000);

Once we know that the notice has been sent out, we can use the sys_watermark table to locate the information that we need to link the notice record to the associated email record.

// update the references to the sent email
var sleeper = new global.Sleeper();
sleeper.sleep(10000);
noticeGR.initialize();
noticeGR.addQuery('review_execution', executionGR.getUniqueValue());
noticeGR.query();
while (noticeGR.next()) {
	var watermark = noticeGR.getValue('email_watermark');
	if (watermark) {
		var watermarkGR = new GlideRecord('sys_watermark');
		if (watermarkGR.get('number', watermark.substring(4))) {
			noticeGR.setValue('email', watermarkGR.getValue('email'));
			noticeGR.update();
		}
	}
}

That should wrap up the process for sending out the requests to review the artifacts. At this point our entire Script Include now looks like this:

var PeriodicReviewUtils = Class.create();
PeriodicReviewUtils.prototype = {
    initialize: function() {
    },

	dailyProcess: function() {
		var toRun = [];
		var configurationGR = new GlideRecord('x_11556_periodic_r_review_configuration');
		var today = new GlideDate();
		configurationGR.addQuery('next_scheduled_date', today);
		configurationGR.orderBy('number');
		configurationGR.query();
		while (configurationGR.next()) {
			var execution = {};
			execution.configuration = configurationGR.getUniqueValue();
			execution.table = configurationGR.getDisplayValue('table');
			execution.filter = configurationGR.getDisplayValue('filter');
			toRun.push(execution);
		}
		if (toRun.length > 0) {
			gs.info('PeriodicReviewUtils.dailyProcess: Running ' + toRun.length + ' execution(s) today.');
			for (var i in toRun) {
				var thisRun = toRun[i];
				this.processExecution(thisRun.configuration, thisRun.table, thisRun.filter);
			}
			gs.info('PeriodicReviewUtils.dailyProcess: ' + toRun.length + ' execution(s) completed.');
		} else {
			gs.info('PeriodicReviewUtils.dailyProcess: Nothing scheduled to run today.');
		}
	},

	processExecution: function(configuration, table, filter) {
		gs.info('PeriodicReviewUtils.processExecution: ' + configuration + '; ' + table + '; ' + filter);

		// fetch system properties
		var systemFallbackAddress = gs.getProperty('x_11556_periodic_r.fallback_email_address');
		var systemEmailDomain = gs.getProperty('x_11556_periodic_r.email_domain');

		// create execution record
		var executionGR = new GlideRecord('x_11556_periodic_r_review_execution');
		executionGR.configuration = configuration;
		executionGR.run_date = new GlideDate();
		executionGR.run_start = new GlideDateTime();
		executionGR.state = 'Running';
		executionGR.short_description = executionGR.getDisplayValue('run_date') + ' review notices';
		executionGR.total_items = 0;
		executionGR.total_notices = 0;
		executionGR.completion_code = 0;
		executionGR.insert();
		var configurationGR = executionGR.configuration.getRefRecord();
		var fallbackRecipient = configurationGR.getDisplayValue('fallback_recipient');

		// create temporary notice record
		var noticeGR = new GlideRecord('x_11556_periodic_r_review_notice');
		noticeGR.review_execution = executionGR.getUniqueValue();
		noticeGR.recipient = configurationGR.fallback_recipient;
		noticeGR.short_description = 'Temporary notice record';
		noticeGR.insert();
		var tempNotice = noticeGR.getUniqueValue();

		// create notice item records from source table data
		var noticeItemGR = new GlideRecord('x_11556_periodic_r_review_notice_item');
		var itemCt = 0;
		var sourceGR = new GlideRecord(table);
		if (sourceGR.isValid()) {
			if (filter) {
				sourceGR.addEncodedQuery(filter);
			}
			sourceGR.orderBy(configurationGR.short_description_column);
			sourceGR.query();
			while (sourceGR.next()) {
				noticeItemGR.initialize();
				noticeItemGR.review_notice = tempNotice;
				noticeItemGR.id = sourceGR.getUniqueValue();
				noticeItemGR.short_description =  sourceGR.getDisplayValue(configurationGR.short_description_column);
				noticeItemGR.description = sourceGR.getDisplayValue(configurationGR.description_column);
				noticeItemGR.recipient = sourceGR.getValue(configurationGR.recipient_column);
				noticeItemGR.insert();
				itemCt++;
				var recipientGR = new GlideRecord('sys_user');
				recipientGR.get(noticeItemGR.getValue('recipient'));
				var notes = '';
				if (recipientGR.isValid()) {
					if (recipientGR.getValue('active')) {
						var email = recipientGR.getDisplayValue('email');
						if (email) {
							if (systemEmailDomain && !email.endsWith(systemEmailDomain)) {
								notes = 'Recipient email address is not an authoized email address; reverting to fallback recipient';
							}
						} else {
							notes = 'Specified recipient has no email address; reverting to fallback recipient';
						}
					} else {
						notes = 'Specified recipient is not active; reverting to fallback recipient';
					}
				} else {
					notes = 'Recipient column empty on source record; reverting to fallback recipient';
				}
				if (notes) {
					noticeItemGR.notes = notes;
					noticeItemGR.recipient = fallbackRecipient;
					noticeItemGR.update();
				}
			}
			if (itemCt > 0) {
				executionGR.total_items = itemCt;
				executionGR.update();
				this.sendNotices(configurationGR, executionGR, noticeGR, noticeItemGR, tempNotice);
			} else {

				// finalize the execution
				executionGR.total_items = 0;
				executionGR.total_notices = 0;
				executionGR.run_end = new GlideDateTime();
				executionGR.state = 'Complete';
				executionGR.completion_code = 0;
				executionGR.description = 'No items matched the filter criteria during this run, so no notices were sent out.';
				executionGR.update();
			}

			// delete the temporary notice
			noticeGR.get(tempNotice);
			noticeGR.deleteRecord();
		} else {

			// finalize the execution
			executionGR.total_items = 0;
			executionGR.total_notices = 0;
			executionGR.run_end = new GlideDateTime();
			executionGR.state = 'Failed';
			executionGR.completion_code = 1;
			executionGR.description = 'The specified source table in the configuration record is not valid; execution cannot proceed; aborting execution.';
			executionGR.update();
		}

		// set the next run date
		configurationGR.setValue('next_scheduled_date', this.calculateNextRunDate(configurationGR));
		configurationGR.update();
	},

	sendNotices: function(configurationGR, executionGR, noticeGR, noticeItemGR, tempNotice) {
		gs.info('PeriodicReviewUtils.sendNotices: ' + configurationGR.getUniqueValue() + '; ' + executionGR.getUniqueValue() + '; ' + noticeGR.getUniqueValue() + '; ' + noticeItemGR.getUniqueValue() + '; ' + tempNotice);
		var noticeCt = 0;

		var noticeItemGA = new GlideAggregate('x_11556_periodic_r_review_notice_item');
		noticeItemGA.addQuery('review_notice', tempNotice);
		noticeItemGA.addAggregate('COUNT');
		noticeItemGA.groupBy('recipient');
		noticeItemGA.orderBy('recipient');
		noticeItemGA.query();
		while (noticeItemGA.next()) {
			var recipient = noticeItemGA.getValue('recipient');
			noticeGR.initialize();
			noticeGR.review_execution = executionGR.getUniqueValue();
			noticeGR.recipient = recipient;
			noticeGR.short_description = executionGR.getDisplayValue('run_date') + ' review notice for ' + configurationGR.getDisplayValue('short_description');
			noticeGR.insert();
			noticeItemGR.initialize();
			noticeItemGR.addQuery('recipient', recipient);
			noticeItemGR.addQuery('review_notice', tempNotice);
			noticeItemGR.query();
			while (noticeItemGR.next()) {
				noticeItemGR.review_notice = noticeGR.getUniqueValue();
				noticeItemGR.update();
			}
			// now you need to send out the notice, passing in the notice record for variables
			gs.eventQueue('x_11556_periodic_r.ReviewNotice', noticeGR, noticeGR.recipient, noticeGR.getUniqueValue());
			noticeCt++;
		}

		// finalize the execution
		var itemLabel = 'item';
		if (executionGR.total_items > 1) {
			itemLabel = 'items';
		}
		var noticeLabel = 'notice was';
		if (noticeCt > 1) {
			noticeLabel = 'notices were';
		}
		executionGR.total_notices = noticeCt;
		executionGR.run_end = new GlideDateTime();
		executionGR.state = 'Complete';
		executionGR.completion_code = 0;
		executionGR.description = noticeCt + ' ' + noticeLabel + ' generated and sent out covering a total of ' + executionGR.total_items + ' ' + itemLabel + '.';
		executionGR.update();

		// update the references to the sent email
		var sleeper = new global.Sleeper();
		sleeper.sleep(10000);
		noticeGR.initialize();
		noticeGR.addQuery('review_execution', executionGR.getUniqueValue());
		noticeGR.query();
		while (noticeGR.next()) {
			var watermark = noticeGR.getValue('email_watermark');
			if (watermark) {
				var watermarkGR = new GlideRecord('sys_watermark');
				if (watermarkGR.get('number', watermark.substring(4))) {
					noticeGR.setValue('email', watermarkGR.getValue('email'));
					noticeGR.update();
				}
			}
		}
	},

	calculateNextRunDate: function(configurationGR) {
        var runDate = new Date();
        var frequency = configurationGR.getValue('frequency');
        var days = 0;
        var months = 0;
        if (frequency == 'daily') {
            days = 1;
        } else if (frequency == 'weekly') {
            days = 7;
        } else if (frequency == 'biweekly') {
            days = 14;
        } else if (frequency == 'monthly') {
            months = 1;
        } else if (frequency == 'bimonthly') {
            months = 2;
        } else if (frequency == 'quarterly') {
            months = 3;
        } else if (frequency == 'semiannually') {
            months = 6;
        } else if (frequency == 'annually') {
            months = 12;
        } else if (frequency == 'biannually') {
            months = 24;
        }
        if (days > 0) {
            runDate.setDate(runDate.getDate() + days);
        } else {
            runDate.setMonth(runDate.getMonth() + months);
        }
        return JSON.stringify(runDate).substring(1, 11);
    },

    type: 'PeriodicReviewUtils'
};

We still have to build the content for the notices, and a way for the notice recipients to communicate their responses back to the system so that the appropriate action can be taken, so let’s jump into all of that next time out.

Periodic Review, Part IX

“Hide not your Talents, they for Use were made. What’s a Sun-Dial in the shade!”
Benjamin Franklin

Last time, we wrapped up most of the work on the script that will handle the review process right up to the point where we need to send out the notice to the recipient. Today we will look at one way to send out an email notification and then build the notice that we will want to send out.

One of the easiest ways to trigger an outbound email is through the use of a System Event, not to be confused with an Event Management Event, which is an entirely different animal. And neither one of those is related in any way to a ServiceNow Event, but now we are really getting off track. To create a new Event, we will navigate to the Event Registry and then click on the New button.

New System Event

Once we have created our new event, we can create an Email Notification and have the notification triggered by this event. To create our new Email Notification, we will navigate to All > System Notification > Email > Notifications and click on the New button. At this point, let’s not worry too much about the content of the message and let’s just do enough so that we can test things out and make sure that it all works. Once we establish that the email is actually sent out, we can go back in and create the message body that will work for our requirements.

New Email Notification

Under the When to send tab, we select Event is fired from the Send when options and then we select our new event from the Event name options. Then on the Who will receive tab, we check the box labeled Event parm 1 contains recipient, which will allow us to send in the recipient as one of the event parameters.

Identifying the intended recipient

In the What it will contain tab, we will just put the word Testing in the subject and body for now and then save the record so that we can run a test. Now we need to modify our Script Include to initiate the event, passing in the appropriate parameters, namely the notification record and the intended recipient. We will replace this line that we added for earlier testing:

gs.info('This is where we would send a notice to ' + noticeGR.getDisplayValue('recipient'));

… with this new code to add a new instance of the event to the queue:

// now you need to send out the notice, passing in the notice record for variables
gs.eventQueue('x_11556_periodic_r.ReviewNotice', noticeGR, noticeGR.recipient, noticeGR.getUniqueValue());
noticeCt++;

After we save that we can pop back over to Scripts – Background and see if all of this results in some email being sent out.

New test results

Well, that looks pretty good, but let’s take a look at the email logs and see if we actually sent out some notices.

Notification emails generated

OK, that works! Now that we know that our process will send out the notices to the designated recipients, the next thing that we will need to do is to come up with the content of the notice. That sounds like a good project for our next installment.

Collaboration Store, Part VIII

“Fit the parts together, one into the other, and build your figure like a carpenter builds a house. Everything must be constructed, composed of parts that make a whole.”
Henri Matisse

With the first of the two referenced Script Include methods now completed, we can turn our attention to the second one, which is responsible for sending out the Email Notification containing the email verification code. Before we get into that, though, we should first come up with a way to generate a random code. In Javascript, you can use the Math.random() function to generate a random number between 0 (inclusive), and 1 (exclusive). Coupled with the Math.floor() function and a little multiplication, you can generate a single numeric digit (0-9):

var singleDigit = Math.floor(Math.random() * 10);

So, if we want a random 6-digit numeric code, we can just loop through that process multiple times to build up a 6 character string:

var oneTimeCode = '';
for (var i=0; i<6; i++) {
	oneTimeCode += Math.floor(Math.random() * 10);
}

That was pretty easy. Now that we have the code, what should we do with it? Since our goal is to send out a notification, the easiest thing to do would be to trigger a System Event and include the recipient’s email address and our random code. In order to do that, we must first create the Event. To do that, navigate to System Policy > Events > Registry to bring up the list of existing Events and then click on the New button to bring up the form.

The Event Registry form

For our purposes, the only field that we need to fill out is the Suffix, which we will set to email_verification. That will generate the actual Event name, which will include the Application Scope. It is the full Event name that we will have to reference in our script when we trigger the Event. To do that, we will use the GlideSystem eventQueue() method. And that’s all we have to do other than to return the random code that we generated back to the calling widget. Here is the complete Script Include function:

verifyInstanceEmail: function(email) {
	var oneTimeCode = '';

	for (var i=0; i<6; i++) {
		oneTimeCode += Math.floor(Math.random() * 10);
	}
	gs.eventQueue('x_11556_col_store.email_verification', null, email, oneTimeCode);

	return oneTimeCode;
}

Of course, even though we have completed the missing Script Include function, our work is not complete. We still have to build a Notification that will be triggered by the Event. To create a new Notification, navigate to System Notification -> Email -> Notifications to bring up the list of existing Notifications, and then click on the New button to create a new Notification.

Collaboration Store Email Verification Notification

I named this one the Collaboration Store Email Verification, set the Type to EMAIL, and checked the Active checkbox. Under the When to send tab, we set the Send when value to Event is fired, and in the Event name field, we enter the full name of our new System Event.

Notification form When to send tab

In the Who will receive tab, we check the Exclude delegates, Event parm 1 contains recipient, and Send to event creator checkboxes. That last one is especially important, as it will usually be the person doing the set-up that owns the email address, and if that box is not checked, the email will not be sent.

Notification form Who will receive tab

The last tab to fill out is the What will it contain tab. Here is where we will build the subject and body of the email that will be sent out. We don’t really need all that much here, since all we are trying to do is to get them the code that we generated, but here is the text that I came up for the body:

Here is the security code that you will need to complete the set-up of the Collaboration Store application:

${event.parm2}

That last line pulls the code out of the Event that was fired and places it in the body of the message. For the Subject, I cut and pasted the same text that I had used for the name of the Notification, Collaboration Store Email Verification.

Notification form What will it contain tab

Once you Save the Notification, you can test it out using a generated Event or an actual Event, just to see how it all comes out. To really test everything end to end, you can pull up the Set-up widget and enter in the details for a new instance. The email should then go out if all is working as it should. Just don’t fill the code in on the form just yet, as that would trigger the final set-up process, and we haven’t built that just yet. But we should get started on that, next time out.

Collaboration Store, Part V

“Do not be embarrassed by your failures; learn from them and start again.”
Richard Branson

With the completion of the client side code, it is now time to turn our attention to a much bigger effort, all of the things that will need to go on over on the server side. This will involve a number of items beyond just the widget itself, but we can start with the widget and branch out from there. One thing that I know I will need for sure is a Script Include to house all of the various common routines, so I built out an empty shell of that, just to get things started.

var CollaborationStoreUtils = Class.create();
CollaborationStoreUtils.prototype = {
    initialize: function() {
    },

	type: 'CollaborationStoreUtils'
};

That’s enough to reference the script in the widget, which we should do right out of the gate, along with gathering up a couple of our application’s properties and checking to make sure that the set-up process hasn’t already been completed:

var csu = new CollaborationStoreUtils();
data.registeredHost = gs.getProperty('x_11556_col_store.host_instance');
data.registeredHostName = gs.getProperty('x_11556_col_store.store_name');
var thisInstance = gs.getProperty('instance_name');
var mbrGR = new GlideRecord('x_11556_col_store_member_organization');
if (mbrGR.get('instance', thisInstance)) {
	data.phase = 3;
}

We get the instance name from a stock system property (instance_name) and then see if we can fetch a record from the database for that instance. If we can, then the set-up process has already been completed, and we advance the phase to 3 to bring up the set-up completion screen. The next thing that we do is check for input, and if there is input, then we grab the data that we need coming in from the client side and check the input.action variable (c.data.action on the client side) to see what it is that we have been asked to do.

if (input) {
	data.registeredHost = gs.getProperty('x_11556_col_store.host_instance');
	data.registeredHostName = gs.getProperty('x_11556_col_store.store_name');
	data.phase = input.phase;
	data.instance_type = input.instance_type;
	data.host_instance_id = input.host_instance_id;
	data.store_name = input.store_name;
	data.instance_name = input.instance_name;
	data.email = input.email;
	data.description = input.description;
	data.validationError = false;
	if (input.action == 'save') {
		// save logic goes here ...
	} else if (input.action == 'setup') {
		// set-up logic goes here ...
	}
}

That is the basic structure of the widget, but of course, the devil is in the details. Since the save process comes before the set-up process, we’ll take that one on first.

If you elected to set up a Host instance, then there is nothing more to do at this point other than to send out the email verification notice and advance the phase to 2 so that we can collect the value of the code that was sent out and entered by the user. However, if you elected to set up a Client instance, then we have a little bit of further work to do before we proceed. For one thing, we need to make sure that you did not specify your own instance name as the host instance, as you cannot be a client of your own host. Assuming that we passed that little check, the next thing that we need to do is to check to see if the host that you specified is, in fact, an actual Collaboration Store host. That will take a bit of REST API work, but for today, we will assume that there is a function in our Script Include that can make that call. To complete the save action, we can also assume that there is another Script Include function that handles the sending out of the Notification, which will allow us to wrap up the save action logic as far as the widget is concerned.

if (data.instance_type == 'client') {
	if (data.host_instance_id == thisInstance) {
		gs.addErrorMessage('You cannot specify your own instance as the host instance');
		data.validationError = true;
	} else {
		var resp = csu.getStoreInfo(data.host_instance_id);
		if (resp.responseCode == '200' && resp.name > '') {
			data.store_name = resp.name;
			data.store_info = resp.storeInfo.result.info;
		} else {
			gs.addErrorMessage(data.host_instance_id + ' is not a valid Collaboration Store instance');
			data.validationError = true;
		}
	}
}
if (!data.validationError) {
	data.oneTimeCode = csu.verifyInstanceEmail(data.email);
}

So now we have referenced two nonexistent Script Include functions to get through this section of code. We should build those out next, just to complete things, but neither one is an independent function. The getStoreInfo function needs to call a REST service, which also doesn’t exist, and the verifyInstanceEmail function needs to trigger a notification, which does not exist at this point, either. We should create those underlying services first, and make sure that they work, and then we can build the Script Include functions that invoke them to finish things up.

That seems like quite a bit of work in and of itself, so this looks like a good place to wrap things up for now. We can jump on that initial web service first thing next time out.