Periodic Review, Part VIII

“Code is like humor. When you have to explain it, it’s bad.”
Cory House

Last time, we built out the processExecution function in our utility Script Include, and now we need to continue on by building out the sendNotices function. Before we build it out completely, though, let’s just create a quick stub so that we can test out the work so far. This ought to do the trick.

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

Now we just need to make sure that we have the next run date set correctly on our lone example configuration and then we can navigate over to Scripts – Background and type in our earlier testing code.

var pru = new PeriodicReviewUtils();
pru.dailyProcess();
gs.info('Done!');

And once again we can hit that Run script button and see what happens.

Initial test results

Well, that’s not good! It seems that there is some kind of problem with our calculateNextRunDate function. It’s always something! But then, that’s why I like to test things out as I go rather than waiting for the entire thing to be built to try things out. The good news is that it seems to have run all of the way through and made it all the way to the end, where it updates the run date, before it crashed and burned. Let me dig into that function and see what might be wrong.

(insert brief debugging pause)

OK, I found the issue. Apparently, it doesn’t like this line:

var runDate = new Date(configurationGR.getDisplayValue('next_scheduled_date'));

The next scheduled date on the configuration record is a GlideDate, and the display value is year, month, and then day. The Javascript Date constructor doesn’t like that, although I know that I have done that in the past without issue. Still, it doesn’t really matter because the value should be today’s date; otherwise, we wouldn’t be running right now. So we can simple change that to this:

var runDate = new Date();

That should still get us the date that we are looking for, so let’s hop back over to Scripts – Background and try this again.

Successful test results

That’s better. So far, so good. Now we need to actually build out that sendNotices function to group all of the notice item records by recipient, create a notice record for each recipient, and then send out the notices. We can develop an unduplicated list of recipients by using a GlideAggregate.

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');
	...
}

Once we have the recipient, we can create the notice record for that 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();

Now that a notice record for this recipient has been created, we need to go back and find all of the notice item records that are associated with this recipient and update the reference to the notice record from the temporary notice to the newly created notice for this recipient.

noticeItemGR.initialize();
noticeItemGR.addQuery('recipient', recipient);
noticeItemGR.addQuery('review_notice', tempNotice);
noticeItemGR.query();
while (noticeItemGR.next()) {
	noticeItemGR.review_notice = noticeGR.getUniqueValue();
	noticeItemGR.update();
}

At this point, we should be able to send out the notice, but as that is a rather complex process in and of itself, let’s just stub things out for now so that we can test out the process so far.

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

All together, our new sendNotices function now looks like this.

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();
		}
		gs.info('This is where we would send a notice to ' + noticeGR.getDisplayValue('recipient'));
	}
}

That should be enough to give things the old college try. Let’s pop back over to Scripts – Background and paste in our testing code and see what we get.

Testing results

Nice! Obviously, a lot more testing is in order before we can say for sure that everything works for every scenario, but things are looking pretty good. Next time, let’s see if we can actually send out a notice.

Scripted Value Columns, Enhanced, Part III

“It’s not about ideas. It’s about making ideas happen.”
Scott Belsky

Last time, we played around with the Customer column in our new example data table, and today we will jump back over to the Labor Hours column and see if we can create that mouse-over breakdown of who put in the hours. The first thing that we will need to do is to gather up the data, so will need to add a groupBy to our GlideAggregate so that we can obtain the hours per technician.

var timeGA = new GlideAggregate('time_card');
timeGA.addQuery('task', item.sys_id);
timeGA.addAggregate('SUM', 'total');
timeGA.groupBy('user');
timeGA.orderBy('user');
timeGA.query();

Once we have the data, we will need to format it for display. There are a number of different ways that we can approach this, including straight CSS and many creative variations, but the easiest thing to do is to just use the title attribute of a span, which seems to accomplish the same thing.

Mouseover breakdown of labor hours

To create the text, we can establish some variables before we go into the while loop, and then inside the loop we can build up the text, one person’s hours at a time.

var hours = 0;
var tooltip = '';
var separator = '';
var timeGA = new GlideAggregate('time_card');
timeGA.addQuery('task', item.sys_id);
timeGA.addAggregate('SUM', 'total');
timeGA.groupBy('user');
timeGA.orderBy('user');
timeGA.query();
while (timeGA.next()) {
	var total = timeGA.getAggregate('SUM', 'total') * 1;
	hours += total;
	tooltip += separator + timeGA.getDisplayValue('user') + ' - ' + total.toFixed(2);
	separator = '\n';
}

Once we accumulate the total hours and build up the tooltip text, we can then format the HTML we want, but only when there are hours charged to the task.

if (hours > 0) {
	response = '<span style="text-align: right; width: 100%;" title="' + tooltip + '">' + hours.toFixed(2) + '</span>';
}

Putting it all together, our new function to provide right-justified total hours with a mouseover breakdown looks like this:

getScriptedValue: function(item, config) {
	var response = '';

	var hours = 0;
	var tooltip = '';
	var separator = '';
	var timeGA = new GlideAggregate('time_card');
	timeGA.addQuery('task', item.sys_id);
	timeGA.addAggregate('SUM', 'total');
	timeGA.groupBy('user');
	timeGA.orderBy('user');
	timeGA.query();
	while (timeGA.next()) {
		var total = timeGA.getAggregate('SUM', 'total') * 1;
		hours += total;
		tooltip += separator + timeGA.getDisplayValue('user') + ' - ' + total.toFixed(2);
		separator = '\n';
	}
	if (hours > 0) {
		response = '<span style="text-align: right; width: 100%;" title="' + tooltip + '">' + hours.toFixed(2) + '</span>';
	}

	return response;
}

I think that is enough examples for folks to get the basic idea. Adding the ability to include HTML with a scripted value opens up a number of possibilities. We have just explored a couple of them here, but I am sure that specific requirements will drive many other variations from those willing to give it a try and see what they can do with it.

Here is an Update Set with the modifications, including this new example page. Feedback can be left here in the comments, or in the discussion area where it has been posted out on Share. If you have been able to utilize this feature for anything interesting, a screenshot would definitely be something in which folks would have an interest, so please let us all in on what you were able to accomplish.

Scripted Value Columns, Enhanced

“Never let an inventor run a company. You can never get him to stop tinkering and bring something to market.”
Ernst Friedrich Schumacher

Since I posted a new version of the SNH Data Table Widgets out on Share with the new Scripted Value Columns feature, I have been playing around with it for various purposes, and have decided to make a little tweak to the core Data Table widget to accommodate some additional capabilities. My interest was in having the value provider script have the ability to return HTML as part of the value, which I was able to do, but when I did that, the table would simply display the HTML as text rather than process it to format the value. So I dug into the HTML in the core SNH Data Table widget and changed this:

<td ng-repeat="obj in item.svcValue" role="cell" class="sp-list-cell" ng-class="{selected: item.selected}" tabindex="0">
  {{obj.value}}
</td>

… to this:

<td ng-repeat="obj in item.svcValue" role="cell" class="sp-list-cell" ng-class="{selected: item.selected}" tabindex="0">
  <span ng-bind-html="obj.value"></span>
</td>

That sort of worked, but it still stripped out some portions of the HTML for safety reasons. To get around that problem, I had to change things to this:

<td ng-repeat="obj in item.svcValue" role="cell" class="sp-list-cell" ng-class="{selected: item.selected}" tabindex="0">
  <span ng-bind-html="trustAsHtml(obj.value)"></span>
</td>

… and then add this new function to the Client script of the widget:

$scope.trustAsHtml = function(string) {
	return $sce.trustAsHtml(string);
};

That was better, but still not exactly what I wanted, because when I tried to right justify some numeric values, they still came out on the left. The data was on the right side of the span, but the span was only as wide as the data, so that really did not do what I wanted. I wanted the data to be on the right side of the table cell, not the span inside of the table cell. However, I was able to solve that problem with a little extra style magic, so now I had this.

<td ng-repeat="obj in item.svcValue" role="cell" class="sp-list-cell" ng-class="{selected: item.selected}" tabindex="0">
  <span ng-bind-html="trustAsHtml(obj.value)" style="display: flex;"></span>
</td>

Even better, but still not exactly what I wanted. With the display set to flex, the carriage returns above and below the span end up rendered in the cell along with the (finally!) right-justified value. To solve that problem, I ended up putting the td, the span, and the closing td tags all on one line with no white space of any kind in between. Now I finally had what I was after. I just needed to do a little trial and error to see what I could do with it.

I was working on a list of assigned Tasks for any given Assignment Group, and one of the columns that I wanted to include on the list was the number of hours spent on the tasks so far. To get the value, I used a GlideAggregate on the time_card table.

var hours = 0;
var timeGA = new GlideAggregate('time_card');
timeGA.addQuery('task', item.sys_id);
timeGA.addAggregate('SUM', 'total');
timeGA.query();
if (timeGA.next()) {
	hours = timeGA.getAggregate('SUM', 'total');
}

That gave me the number of hours charged to the task from anyone from any group, whether or not the Time Card had been approved. I could have run a different query for a different number, but this was the data that I wanted to display on the table. Returning the hours value alone aligned the column on the left, though, and I wanted the values to be right justified. With the modifications made to core data table widget above, I was able to wrap the value with a right justified span and obtain the result that I wanted to see.

return '<span style="text-align: right; width: 100%;">' + (hours * 1).toFixed(2) + '</span>';

Things just look better with numbers lined up on the right.

Hours column lined up on the right

I like it, but there are still some things that we could do to make it better. It would be nice to know who put in these hours. Maybe a tooltip with a breakdown or a modal pop-up with the details would be nice. Also, it might be nice to have nothing at all in the column if the value is zero. That would make the rows with values stand out a little more. So many possibilities …

Oh, and we haven’t even begun to take a look at that Customer column just yet. Let’s play around with that and more next time out.

Aggregate List Columns, Part III

“I want to try it to see what it’s like and see what my stuff looks like when I take it from inception to completion.”
Charlie Kaufman

Last time, we started the work of modifying the core widget of the data table widget collection. We took care of all of the code that imports the configuration data from the various wrapper widgets in the collection, and now we need to get down to the business of actually putting real data in the new aggregate list columns. To begin, we need to take a look at the code that pulls in the regular data for each row in the list.

	data.list = [];
	while (gr._next()) {
		var record = {};
		$sp.getRecordElements(record, gr, data.fields);
		if (typeof FilteredGlideRecord != 'undefined' && gr instanceof FilteredGlideRecord) {
			// FilteredGlideRecord doesn't do field-level
			// security, so take care of that here
			for (var f in data.fields_array) {
				var fld = data.fields_array[f];
				if (!gr.isValidField(fld))
					continue;

				if (!gr[fld].canRead()) {
					record[fld].value = null;
					record[fld].display_value = null;
				}
			}
		}
		for (var f in data.fields_array) {
			var fld = data.fields_array[f];
			if (record[fld].type == 'reference') {
				var refGr = gr;
				var refFld = fld;
				if (fld.indexOf('.') != -1) {
					var parts = fld.split('.');
					for (var x=0;x<parts.length-1;x++) {
						refGr = refGr[parts[x]].getRefRecord();
					}
					refFld = parts[parts.length-1];
				}
				if (refGr.isValidField(refFld)) {
					record[fld].table = refGr.getElement(refFld).getED().getReference();
					record[fld].record = {type: 'reference', sys_id: {value: record[fld].value, display_value: record[fld].value}, name: {value: record[fld].display_value, display_value: record[fld].display_value}};
				}
			}
		}
		record.sys_id = gr.getValue('sys_id');
		record.targetTable = gr.getRecordClassName();
		data.list.push(record);
	}

Basically, this code creates an array called data.list and then for each row creates an object called record, populates the record object from the GlideRecord, and then pushes the record object into the list. What we will want to do is add data to the record object before it gets pushed onto the list. The sys_id of the record will actually be useful to us, so it seems as if the best place to insert our code would be after the sys_id is established, but before the data.list.push(record) occurs. Here is what I came up with:

record.aggValue = [];
if (data.aggarray.length > 0) {
	for (var j=0; j<data.aggarray.length; j++) {
		record.aggValue.push(getAggregateValue(record.sys_id, data.aggarray[j]));
	}
}

To store the values, I added a value list to the record object. This is done whether or not any aggregate columns have been defined, just so there is something there regardless. Then we check to see if there actually are any aggregate columns defined, and if so, we loop through the definitions and then push a value onto the array for each definition. The value itself will be determined by a new function called getAggregateValue that takes the sys_id of the row and the aggregate column definition as arguments. We will need to build out that function, but for now, we can just return a hard-coded value, just to make sure that all is working before we dive into that.

function getAggregateValue(sys_id, config) {
	return 10;
}

Now that we have an array of values, we will need to go back into the HTML and modify the aggregate column section to pull data from this array. That section of the HTML now looks like this:

<td ng-repeat="aggValue in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{aggValue}}
</td>

That should be enough to take it out for a spin and see if we broke anything. Let’s have a look.

Test page with hard-coded value array

Cool! So far, so good. Now we just need to go back into that getAggregateValue function definition and replace the hard-code value of 10 with some actual logic to pull the real data out of the database. For that, we will use a GlideAggregate on the configured table using the configured field and the sys_id of the current row. And if there is an optional filter present, we simply concatenate that to the end of the primary query.

function getAggregateValue(sys_id, config) {
	var value = 0;
	var ga = new GlideAggregate(config.table);
	ga.addAggregate('COUNT');
	var query = config.field + '=' + sys_id;
	if (config.filter) {
		query += '^' + config.filter;
	}
	ga.addEncodedQuery(query);
	ga.query();
	if (ga.next()) {
		value = parseInt(ga.getAggregate('COUNT'));
	}
	return value;
}

Now we can take another quick look and see if that finally gets us what we have been after all along.

Finally, the result we have been looking for

Voila! There it is — a column containing a count of selected related records. Now that wasn’t so hard, was it? Of course, we are not done quite yet. The SNH Data Table from JSON Configuration widget is not the only wrapper widget in the collection. We will need to add support for aggregate columns to the SNH Data Table from Instance Definition widget, as well as the SNH Data Table from URL Definition widget, the widget designed to work with the Configurable Data Table Widget Content Selector. Also, we have the Content Selector Configuration Editor, which allows you to create the JSON configuration files for both the Configurable Data Table Widget Content Selector and the SNH Data Table from JSON Configuration widget. That will have to be modified to support aggregate column specifications as well. None of that is super challenging now that we have things working, but it all needs to be done, so we will jump right into that next time out.

Adding Detail to the Upgrade History Task

“Ask not that the journey be easy; ask instead that it be worth it.”
John F. Kennedy

With the addition of the Paris Upgrade Center, a new type of Task was introduced, the Upgrade History Task. Whenever you upgrade your instance, issues that require your attention will now produce an Upgrade History Task, which you can assign to the appropriate resources for resolution. This is a nice feature, and fairly well implemented, but the form layout is not quite organized in the way that I would like to see it, and there are some tidbits of data from various other, related tables that I would really like to see all in one place. Another thing that annoys me is that the Upgrade Details Related Link opens up a new window and breaks out of the iFrame, losing the header and sidebar menu. That page has a link back to the Upgrade History Task, and if you click back and forth a few times, suddenly you have all kinds of windows open, and none of them have navigation anymore. I don’t like that.

So, I thought about making a bunch of UI Formatters and rearranging the stock form layout to include all of the information that I like to see when I am working an upgrade issue, but the more upgrades that I work on, the less I like to tinker with stock components. Ultimately, I decided to just add a single UI Action that would pop up a modal dialog box that contained the information that I was looking for. Here are the things that I wanted to see:

  • A standard link to the Skip record that didn’t open up a new window,
  • The name of the affected table,
  • A link to the affected record,
  • When the affected record was last updated,
  • Who last updated the affected record,
  • Any indication that we have dealt with this issue before, and if so,
  • The details of what was done with this the last time that it came up in an upgrade.

None of that is present on the form right now, but I didn’t see any reason that we couldn’t pull it all together from the various sources, so I went to work. It seemed like there would be a bit of database querying to get all of this information, and I didn’t really want all of that on the page itself, so I started out by making myself a little Script Include to house all of major code. I called it UpgradeTaskUtils and created a single function called getAdditionalInfo to pull together all of the data.

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

    getAdditionalInfo: function(sysId) {
		var details = {};

		// all of the hard work goes here!

		return details;
	},

	type: 'UpgradeTaskUtils'
};

The first thing that we will need to do, of course, is to use the incoming sysId argument to go out and get the Upgrade History Record, and once we do that, we can then pull out the associated Skip record.

details.taskGR = new GlideRecord('upgrade_history_task');
if (details.taskGR.get(sysId)) {
	details.logGR = details.taskGR.upgrade_detail.getRefRecord();
	if (details.logGR && details.logGR.isValidRecord()) {
		// more code to follow ...
	}
}

Once we know that we have a valid Skip record, the next thing that we will want to do is go get the actual record that has the issue. That’s a little more complicated and uses a table called sys_metadata.

var metaGR = new GlideRecord('sys_metadata');
if (metaGR.get('sys_update_name', details.logGR.file_name.toString())) {
	details.recordGR = new GlideRecord(metaGR.sys_class_name);
	if (details.recordGR.get(metaGR.sys_id)) {
		details.lastRecordUpdate = details.recordGR.getDisplayValue('sys_updated_on');
		details.lastRecordUpdateBy = details.recordGR.getDisplayValue('sys_updated_by');
		// more code to follow ...
	}
}

Since the sys_updated_by fields is just a user_name string and not an actual reference to a User record, if we want to have the details on the User who last updated the record, we will need to go out and fetch that separately.

var userGR = new GlideRecord('sys_user');
if (userGR.get('user_name', details.lastRecordUpdateBy)) {
	details.userName = userGR.getDisplayValue('name');
	details.userSysId = userGR.getUniqueValue();
	details.userLink = '<a href="/sys_user.do?sys_id=' + details.userSysId + '">' + details.userName + '</a>';
}

That takes care of all of the interrelated records involved with this Task, but there is still more work to do if we want any historical data for this same artifact in previous upgrades. Basically, we want to find all of the records in the Upgrade History Task table that reference this same component, except for the one that we already have. We can just do a quick count to start off with, just to see if there is any point in looking any further.

details.previousIssues = 0;
details.prevIssueQuery = 'upgrade_detail.file_name=' + details.taskGR.upgrade_detail.file_name + '^number!=' + details.taskGR.number;
var taskGA = new GlideAggregate('upgrade_history_task');
taskGA.addAggregate('COUNT');
taskGA.addEncodedQuery(details.prevIssueQuery);
taskGA.query();
if (taskGA.next()) {
	details.previousIssues = taskGA.getAggregate('COUNT');
}

Now that we have a count of what’s out there, we can gather up all of the details if the count is greater than zero.

details.prevIssueLink = details.previousIssues;
if (details.previousIssues > 0) {
	details.prevIssueLink = '<a href="/upgrade_history_task_list.do?sysparm_query=' + details.prevIssueQuery + '">' + details.previousIssues + '</a>';
	var taskGR = new GlideRecord('upgrade_history_task');
	taskGR.addEncodedQuery(details.prevIssueQuery);
	taskGR.orderByDesc('sys_created_on');
	taskGR.query();
	if (taskGR.next()) {
		details.previousUpgrade = taskGR.getDisplayValue('upgrade_detail.upgrade_history.to_version');
		details.previousComments = taskGR.getDisplayValue('upgrade_detail.comments');
	}
}

That’s everything that I am looking for right at the moment. I may end up going back one day and tossing in a few more items, but for now, this should do the trick. All together, the new Script Include looks like this:

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

    getAdditionalInfo: function(sysId) {
		var details = {};

		details.taskGR = new GlideRecord('upgrade_history_task');
		if (details.taskGR.get(sysId)) {
			details.logGR = details.taskGR.upgrade_detail.getRefRecord();
			if (details.logGR && details.logGR.isValidRecord()) {
				var metaGR = new GlideRecord('sys_metadata');
				if (metaGR.get('sys_update_name', details.logGR.file_name.toString())) {
					details.recordGR = new GlideRecord(metaGR.sys_class_name);
					if (details.recordGR.get(metaGR.sys_id)) {
						details.lastRecordUpdate = details.recordGR.getDisplayValue('sys_updated_on');
						details.lastRecordUpdateBy = details.recordGR.getDisplayValue('sys_updated_by');
						var userGR = new GlideRecord('sys_user');
						if (userGR.get('user_name', details.lastRecordUpdateBy)) {
							details.userName = userGR.getDisplayValue('name');
							details.userSysId = userGR.getUniqueValue();
							details.userLink = '<a href="/sys_user.do?sys_id=' + details.userSysId + '">' + details.userName + '</a>';
						}
					}
				}
			}
			details.previousIssues = 0;
			details.prevIssueQuery = 'upgrade_detail.file_name=' + details.taskGR.upgrade_detail.file_name + '^number!=' + details.taskGR.number;
			var taskGA = new GlideAggregate('upgrade_history_task');
			taskGA.addAggregate('COUNT');
			taskGA.addEncodedQuery(details.prevIssueQuery);
			taskGA.query();
			if (taskGA.next()) {
				details.previousIssues = taskGA.getAggregate('COUNT');
			}
			details.prevIssueLink = details.previousIssues;
			if (details.previousIssues > 0) {
				details.prevIssueLink = '<a href="/upgrade_history_task_list.do?sysparm_query=' + details.prevIssueQuery + '">' + details.previousIssues + '</a>';
				var taskGR = new GlideRecord('upgrade_history_task');
				taskGR.addEncodedQuery(details.prevIssueQuery);
				taskGR.orderByDesc('sys_created_on');
				taskGR.query();
				if (taskGR.next()) {
					details.previousUpgrade = taskGR.getDisplayValue('upgrade_detail.upgrade_history.to_version');
					details.previousComments = taskGR.getDisplayValue('upgrade_detail.comments');
				}
			}
		}

		return details;
	},

	type: 'UpgradeTaskUtils'
};

Now we need put all of this data on page that we can use for our modal pop-up. I created a new UI Page called upgrade_history_task_info, and started it out by calling our new Script Include to obtain the data.

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
	<g:evaluate var="jvar_not_used">
var utu = new UpgradeTaskUtils();
var obj = utu.getAdditionalInfo(RP.getWindowProperties().get('task_sys_id'));
obj.lastRecordUpdate;		
	</g:evaluate>
	<div style="padding: 20px;">
		<!-- display grid goes here -->
	</div>
</j:jelly>

To format the data, I just used a basic HTML table, with one column for the labels and another for the data.

<table>
	<tr>
		<th style="padding: 5px;">Issue Details:</th>
		<td style="padding: 5px;"><a href="/${obj.logGR.getTableName()}.do?sys_id=${obj.logGR.getUniqueValue()}">${obj.logGR.getDisplayValue()}</a></td>
	</tr>
	<tr>
		<th style="padding: 5px;">Affected Table:</th>
		<td style="padding: 5px;">${obj.recordGR.getLabel()} (${obj.recordGR.getTableName()})</td>
	</tr>
	<tr>
		<th style="padding: 5px;">Affected Record:</th>
		<td style="padding: 5px;"><a href="/${obj.recordGR.getTableName()}.do?sys_id=${obj.recordGR.getUniqueValue()}">${(obj.recordGR.getDisplayValue() > ''?obj.recordGR.getDisplayValue():obj.recordGR.getUniqueValue())}</a></td>
	</tr>
	<tr>
		<th style="padding: 5px;">Record Last Updated:</th>
		<td style="padding: 5px;">${obj.recordGR.getDisplayValue('sys_updated_on')}</td>
	</tr>
	<tr>
		<th style="padding: 5px;">Record Last Updated By:</th>
		<td style="padding: 5px;"><g:no_escape>${(obj.userSysId > ''?obj.userLink:obj.recordGR.getDisplayValue('sys_updated_by'))}</g:no_escape></td>
	</tr>
	<tr>
		<th style="padding: 5px;">Previous Upgrade Issues:</th>
		<td style="padding: 5px;"><g:no_escape>${obj.prevIssueLink}</g:no_escape></td>
	</tr>
	<j:if test="${obj.previousIssues > 0}">
		<tr>
			<th style="padding: 5px;">Last Upgrade w/Issue:</th>
			<td style="padding: 5px;">${obj.previousUpgrade}</td>
		</tr>
		<tr>
			<th style="padding: 5px;">Last Upgrade Comments:</th>
			<td style="padding: 5px;">${obj.previousComments}</td>
		</tr>
	</j:if>
</table>

Now we have our data and we have it laid out nicely on a page, all that’s left to do is to pop it up on the screen. For that, we will need to build a UI Action. I called mine Additional Info, linked it to the Upgrade History Task table, and gave it the following onClick script:

function openAdditionalInfo() {
	var dialog = new GlideDialogWindow('upgrade_history_task_info');
	dialog.setSize(600, 600);
	dialog.setTitle('Additional Info');
	dialog.setPreference('task_sys_id', NOW.sysId);
	dialog.render();
}

That’s pretty much all there is to that. We still need to pull it up and click on it and see what happens, but assuming that all goes well, this exercise should have produced a nice little tool to make plodding through the skipped records in an upgrade just a little bit easier.

Additional Info modal pop-up on the Upgrade History Task form

Nice! Now all we need to do is gather up all of the parts and stuff them into an Update Set.

Fun with Highcharts, Part IV

“Lost wealth may be replaced by industry, lost knowledge by study, lost health by temperance or medicine, but lost time is gone forever.”
Samuel Smiles

Now that we have our generic chart widget, our generic chart object generator, and the choice lists and default selections for our example chart, we just need to come up with the code to gather up the data for the chart based on the selections. A number of elements are based on the period selected, so collecting that data would seem like an important first step. I organized all of that into a function that I called getPeriodData:

function getPeriodData() {
	var periodData = {};
	periodData.frequencyInfo = findOption(data.config.freqOptions, data.frequency);
	periodData.endingDateInfo = findOption(data.config.endingOptions[data.frequency], data.ending);
	periodData.labels = [];
	periodData.endDate = [];
	for (var i=0; i<=periodData.frequencyInfo.size; i++) {
		var dt = new Date(periodData.endingDateInfo.label);
		if (data.frequency == 'd') {
			dt.setDate(dt.getDate() - (periodData.frequencyInfo.size - i));
		} else if (data.frequency == 'w') {
			dt.setDate(dt.getDate() - (periodData.frequencyInfo.size - i) * 7);
		} else if (data.frequency == 'm') {
			dt.setDate(1);
			dt.setMonth(dt.getMonth() - (periodData.frequencyInfo.size - i));
			dt = getLastDayOfMonth(dt);
		} else if (data.frequency == 'q') {
			dt.setDate(1);
			dt.setMonth(dt.getMonth() - (periodData.frequencyInfo.size - i) * 3);
			dt = getLastDayOfMonth(dt);
		} else if (data.frequency == 'y') {
			dt.setFullYear(dt.getFullYear() - (periodData.frequencyInfo.size - i));
		}
		var dtInfo = getDateValues(dt);
		periodData.endDate.push(dtInfo.value);
		if (i > 0) {
			periodData.labels.push(dtInfo.label);
		}
	}			
		
	return periodData;
}

The function collects one more end date than it does labels because the end date of the previous period is used as the start of the current period. You need to be able to go back one extra period to get the end date of a period that you will not actually be using for the start date of the earliest period that you will.

Once you pull together all of the data for the selected frequency and period, you can then use that data to put together everything else needed for the chart. This takes care of the basics:

var chartData = {};
chartData.title = task.getPlural() + ' assigned to ' + findOption(data.config.groupOptions, data.group).label;
chartData.subtitle = periodData.frequencyInfo.label + ' through ' + periodData.endingDateInfo.label;
chartData.labels = periodData.labels;
chartData.received = [];
chartData.completed = [];
chartData.backlog = [];

… and then all that is left is to loop through all of the periods in the chart to run GlideAggregates to compile all of the actual data:

var task = new GlideAggregate(data.type);
for (var i=1; i<periodData.endDate.length; i++) {
	task.initialize();
	task.addAggregate('COUNT');
	task.addEncodedQuery('assignment_group=' + data.group + '^opened_at>' + periodData.endDate[i-1] + '^opened_at<=' + periodData.endDate[i]);
	task.query();
	task.next();
	chartData.received.push(task.getAggregate('COUNT') * 1);
	task.initialize();
	task.addAggregate('COUNT');
	task.addEncodedQuery('assignment_group=' + data.group + '^closed_at>' + periodData.endDate[i-1] + '^closed_at<=' + periodData.endDate[i]);
	task.query();
	task.next();
	chartData.completed.push(task.getAggregate('COUNT') * 1);
	task.initialize();
	task.addAggregate('COUNT');
	task.addEncodedQuery('assignment_group=' + data.group + '^opened_at<=' + periodData.endDate[i] + '^closed_at>' + periodData.endDate[i] + '^ORclosed_atISEMPTY');
	task.query();
	task.next();
	chartData.backlog.push(task.getAggregate('COUNT') * 1);
}

The last thing we need to deal with is the user making new selections from the four pick lists. That’s a client side issue, so we will need a client side script to detect the selections and call for a refresh of the chart.

function($scope, $rootScope) {
	var c = this;
	$scope.updateChart = function() {
		c.server.update().then(function(response) {
			c.data.config = response.config;
			c.data.group = response.group;
			c.data.type = response.type;
			c.data.frequency = response.frequency;
			c.data.ending = response.ending;
			$rootScope.$broadcast('refresh-workload', {chartObject: response.chartObject});
		});
	}
}

I still want to play around with a few more different and interesting chart types, but there are enough parts and pieces now to warrant the assembly of a version 1.0 Update Set. If I ever get a chance to do more, I can always put out a better one later on.