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.

Collaboration Store, Part LXXXIV

“The power of one, if fearless and focused, is formidable, but the power of many working together is better.”
Gloria Macapagal Arroyo

Last time, we released yet another beta version of the app for testing, so now might be a good time to talk a little bit about what exactly needs to be tested, and maybe a little bit about where things stand and where we go from here. We have had a lot of good, quality feedback in the past, and I am hoping for even more from this version. Every bit helps drive out annoying errors and improves the quality of the product, so keep it coming. It is very much appreciated.

Installation

The first thing that needs to be tested, of course, is the installation itself. Just to review, you need to install three Update Sets in the appropriate order, SNH Form Fields, the primary Scoped Application, and the accompanying global components that could not be bundled with the scoped app. You can find the latest version of SNH Form Fields here, or you can simply grab the latest SNH Data Table Widgets from Share, which includes the latest version of the form field tag. Once that has been installed, you can then install collaboration_store_v0.7.5.xml, after which you can then install the globals, collaboration_store_globals_v0.7.xml.

There are two types of installations, a brand new installation and an upgrade of an existing installation. Both types of installs have had various issues reported with both Preview and Commit errors. On a brand new installation, just accept all updates and you should be good to go. I don’t actually know why some of those errors come up on a brand new install, but if anyone knows of any way to keep that from happening, I would love to hear about it. It doesn’t seem to hurt anything, but it would be so much better if those wouldn’t come up at all. On an upgrade to an existing installation, you will want to reject any updates related to system properties. The value of the application’s properties are established during the set-up process, and if you have already gone through the set-up process, you don’t want those values overlaid by the installation of a new version. Everything else can be accepted as is. Once again, if anyone has any ideas on how to prevent that kind of thing from happening, please let us all know in the comments below.

The Set-up Process

The Collaboration Store Set-up process

Once you have the software installed for the first time, you will need to go through the set-up process. This is another thing that needs to be tested thoroughly, both for a Host instance and a Client instance. It needs to tested with logo images and without, and for Client instances, you will need to check all of the other member instances in the community to ensure that the newly set-up instance now appears in each instance. During the set-up process, a verification email will be sent to the email address entered on the form, and if your instance does not actually send out mail, you will need to look in the system email logs for the code that you will need to complete the process.

The Publishing Process

Application Publishing Process

Once the software has been installed and the set-up process completed, you can now publish an application to the store. Both Client instances and Host instances can publish apps to the store. Publishing is accomplished on the system application form via a UI Action link at the bottom of the form labeled Publish to Application Store. Click on the link and follow the prompts to publish your application to the store. If you run into any issues, please report them in the comments below.

The Installation Process

Application Installation Process

Once published to the store, shared applications can be installed by any other Host or Client instance from either the version record of the version desired, or the Collaboration Store itself. Simply click on the Install button and the installation should proceed. Once again, if you run into any issues, please use the comments below to provide us with some detailed information on where things went wrong.

The Collaboration Store

The Collaboration Store

The Collaboration Store page is where you should see all of the applications shared with the community and there are a lot of things that need to be tested here. This is the newest addition to the application, so we will want to test this thing out thoroughly. One thing that hasn’t been tested at all is the paging, as I have never shared enough apps to my own test environment to exceed the page limit. The more the merrier as far as testing is concerned, so if you can add as many Client instances as possible, that would be helpful, and if each Client could share as many applications as possible, that would be helpful as well. Several pages worth, in varying states would help in the testing of the search widget as well as the primary store widget. And again, if you run into any problems, please report them in the comments.

The Periodic Sync Process

The periodic sync process is designed to recover from any previous errors and ensure that all Clients in the community have all of the same information that is stored in the Host. Testing the sync process is simply a matter of removing some artifacts from some Client instance and then running the sync process to see if those artifacts were restored. The sync process runs every day on the Host instance over the lunch hour, but you can also pull up the Flow and run it by hand.

Thanks in advance to those of you who have already contributed to the testing and especially to those of you who have decided to jump in at this late stage and give things a try. Your feedback continues to be quite helpful, and even if you don’t run into any issues, please leave us a comment and let us know that as well. Hopefully, we are nearing the end of this long, drawn out project, and your assistance will definitely help to wrap things up. Next time, we will talk a little bit more about where things go from here.

Collaboration Store, Part LXXXIII

“Many hands make light work.”
John Heywood

Last time, we built a little companion widget to share the page with our storefront widget to provide the ability to filter the list of applications displayed. Clicking on the search button on that widget reloads the page with the search criteria present in the query string of the URL. Now we need to modify the primary widget to pull that search criteria in from the URL and then use it when querying the database for applications to display. To begin, we can use basically the same code that we used in the search widget to bring in the values.

var search = $sp.getParameter('search');
var local = $sp.getParameter('local') == 'true';
var current = $sp.getParameter('current') == 'true';
var upgrade = $sp.getParameter('upgrade') == 'true';
var notins = $sp.getParameter('notins') == 'true';

Now that we have the information, we need to use it to filter the query results. For the search string, we want to look for that string in either the name or the description of the app. That one is relatively straightforward and can be handled with a single encoded query.

var appGR = new GlideRecord('x_11556_col_store_member_application');
if (search) {
	appGR.addEncodedQuery('nameLIKE' + search + '^ORdescriptionLIKE' + search);
}

The remaining parameters are all boolean values that relate to one another in some way, so we have to handle them as a group. First of all, if none of the boxes are checked or all of the boxes are checked, then there is no need for any filtering, so we can eliminate those cases right at the top.

if (local && current && upgrade && notins) {
		// everything checked -- no filter needed
} else if (!local && !current && !upgrade && !notins) {
	// nothing checked -- no filter needed
} else {
	...
}

After that, things get a little more complicated. Local apps are those where the provider instance is the local instance, and we don’t apply any other filter to that pool. You either want the local apps included or you do not. They are included by default, but if you start checking boxes then they are only included if you check the Local checkbox.

var query = '';
var separator = '';
if (local) {
	query += 'provider.instance=' + gs.getProperty('instance_name');
	separator = '^OR';
} else {
	query += separator + 'provider.instance!=' + gs.getProperty('instance_name');
	separator = '^';
}

The remaining three values relate to the apps pulled down from the store, which are classified based on whether they have been installed on the local instance (current and upgrade) or not (notins). Installed apps have a value in the application field and those that have not been installed do not. Additionally, installed apps are considered current if the installed version is the same as the current version; otherwise they are classified as having an upgrade available. We only need to check the version if you want one, but not the other. If you want both current and upgrade or neither current nor upgrade, then there is no point in making that distinction. So we first check both cases where the values are different.

if (current && !upgrade) {
	if (notins) {
		query += separator + 'applicationISEMPTY^ORversionSAMEASapplication.version';
	} else {
		query += separator + 'versionSAMEASapplication.version';
	}
} else if (!current && upgrade) {
	if (notins) {
		query += separator + 'applicationISEMPTY^ORversionNSAMEASapplication.version';
	} else {
		query += separator + 'versionNSAMEASapplication.version';
	}
...

And finally, we check the last two cases where current and upgrade are the same value.

} else if (current && upgrade && !notins) {
	query += separator + 'applicationISNOTEMPTY';
} else if (!current && !upgrade && notins) {
	query += separator + 'applicationISEMPTY';
}

That should do it. Putting that all together with the original fetchItemDetails function gives us this new version of that function.

function fetchItemDetails(items) {
	var search = $sp.getParameter('search');
	var local = $sp.getParameter('local') == 'true';
	var current = $sp.getParameter('current') == 'true';
	var upgrade = $sp.getParameter('upgrade') == 'true';
	var notins = $sp.getParameter('notins') == 'true';
	var appGR = new GlideRecord('x_11556_col_store_member_application');
	if (search) {
		appGR.addEncodedQuery('nameLIKE' + search + '^ORdescriptionLIKE' + search);
	}
	if (local && current && upgrade && notins) {
		// everything checked -- no filter needed
	} else if (!local && !current && !upgrade && !notins) {
		// nothing checked -- no filter needed
	} else {
		var query = '';
		var separator = '';
		if (local) {
			query += 'provider.instance=' + gs.getProperty('instance_name');
			separator = '^OR';
		} else {
			query += separator + 'provider.instance!=' + gs.getProperty('instance_name');
			separator = '^';
		}
		if (current && !upgrade) {
			if (notins) {
				query += separator + 'applicationISEMPTY^ORversionSAMEASapplication.version';
			} else {
				query += separator + 'versionSAMEASapplication.version';
			}
		} else if (!current && upgrade) {
			if (notins) {
				query += separator + 'applicationISEMPTY^ORversionNSAMEASapplication.version';
			} else {
				query += separator + 'versionNSAMEASapplication.version';
			}
		} else if (current && upgrade && !notins) {
			query += separator + 'applicationISNOTEMPTY';
		} else if (!current && !upgrade && notins) {
			query += separator + 'applicationISEMPTY';
		}
		appGR.addEncodedQuery(query);
	}
	appGR.orderBy('name');
	appGR.query();
	while (appGR.next()) {
		var item = {};
		item.name = appGR.getDisplayValue('name');
		item.description = appGR.getDisplayValue('description');
		item.logo = appGR.getValue('logo');
		item.version = appGR.getDisplayValue('current_version');
		item.provider = appGR.getDisplayValue('provider.name');
		item.providerLogo = appGR.provider.getRefRecord().getValue('logo');
		item.local = appGR.getDisplayValue('provider.instance') == gs.getProperty('instance_name');
		item.sys_id = appGR.getUniqueValue();
		item.state = 0;
		if (appGR.getValue('application')) {
			item.state = 1;
			item.installedVersion = appGR.getDisplayValue('application.version');
			if (item.version == item.installedVersion) {
				item.state = 2;
			}
		}
		if (!item.local && item.state != 2) {
			item.attachmentId = getAttachmentId(item.sys_id, item.version);
		}
		items.push(item);
	}
}

Now we can fire up the storefront page and start clicking around and see what we have. Or better yet, we can push out yet another Update Set and let all of you following along at home click around and see if everything works as it should. I always like it when folks with a little different perspective take the time to pull stuff down and give it a whirl, so here you go:

If this is your first time, you will want to take a peek here and here and here. For the rest of you, this is just another 0.7.x drop-in replacement, and you should know what to do by now. Please let us all know what you find in the comments below. Feedback is always welcome and always very much appreciated. Hopefully, we will get some interesting results and we can take a look at those next time out.