Configurable notification agent in #OldNotes

Shockingly, when I arrived at my current company, they had basically NO scheduled agents at all. Apparently, someone had decided long ago that scheduled agents were dangerous, that they would overwhelm and crash the servers. So, whenever anything was done, it was done manually. This even extended to user notifications. That is, if I submitted a document for approval, there was some formula language that would populate a new notification message in the client and the user would fill in any extra details before clicking send. I was shocked. As I’ve modified designs, I’ve been adding background notifications and also scheduled agents. Our main project management database, which our field offices use copies of to manage their projects, hasn’t been mine to modify, since it’s already working and there is a team that customizes the design for each field office.

As we’ve been delving further into XPages and as I’ve been spreading the good word about scheduled agents and notifications, we’re now finally putting them into those project management databases. One hurdle though. Our admin team has, quite rightly, limited who can sign agents that will run on production servers.

Concept

Now, I’ve designed dozens or even hundreds of notification and reminder agents in many databases over the decades, but I always designed them from scratch, customizing it to the particular database and the particular recipients. I’d created a basic one and Ariwan Susey, who’s really coming up to speed on LotusScript and XPages, modified it for use in that project management database. This was nice, and Virginia Tauss had started creating copies of it, customized for each notice type. However, every time someone made a change to the half-dozen agents, I had to sign them. Since they were customized for their particular database and the particular recipients, this meant that eventually, I might spend all day signing agents instead of writing code.

Since the agents were almost the same, except for what view they used and who received the message, I realized that if I created a basic agent, they could use configuration documents to customize as many notices as they wanted and I’d never have to sign that configurable agent again!

Configuration Choices

There were a few basic things I knew would be different between each notification: the view, the recipients, the subject, the server to run on and the time to run. After creating some tests, I also realized that I wanted to emulate the scheduling choices of agents themselves and allow the user to select weekly or monthly notifications instead of just daily. I also remembered that sometimes, they would want to mark the document after they sent the notice, so I made that a configuration choice as well. Based on my recent experience in my Excel series (part 1, part 2, and the sample database) and with full-text queries, I realized we could use those full-text queries in these notifications as well.

So, here’s my form:

AutoNotify Configuration

Since I’ve been fiddling with DXL editing of forms lately, let me include the DXL for that third row for your review. The right cell contains a table for displaying the weekday or day of the month choices, with the hide-whens appropriately.

<tablerow>
	<tablecell><par def='4'>Day(s) to run:</par></tablecell>
	<tablecell>
		<par def='5'>
			<field borderstyle='none' lookupeachchar='false' lookupaddressonrefresh='false'
			type='keyword' kind='editable' name='frequency'>
				<keywords helperbutton='false' recalconchange='true' columns='3' ui='radiobutton'>
					<textlist><text>Daily</text><text>Weekly</text><text>Monthly</text></textlist>
				</keywords>
			</field>
		</par>
		<table leftmargin='0' widthtype='fixedleft' refwidth='2.5000in'>
			<tablecolumn width='1in'/><tablecolumn width='1.5000in'/>
			<tablerow>
				<tablecell valign='center' borderwidth='0px'>
					<pardef id='6' spacebefore='1.5' keepwithnext='true' keeptogether='true'>
						<code event='hidewhen'><formula>frequency != "Weekly"</formula></code>
					</pardef>
					<par def='6'>Day of week: </par>
				</tablecell>
				<tablecell valign='center' borderwidth='0px'>
					<pardef id='7' spacebefore='1.5' keepwithnext='true' keeptogether='true'>
						<code event='hidewhen'><formula>frequency != "Weekly"</formula></code>
					</pardef>
					<par def='7'>
						<field usenotesstyle='false' height='0.2500in' width='1in' multiline='true'
						borderstyle='none' lookupeachchar='false' lookupaddressonrefresh='false'
						type='keyword' kind='editable' name='weekdayToRun'>
							<keywords helperbutton='false' columns='1' ui='combobox'>
								<textlist>
									<text>Sunday|1</text>
									<text>Monday|2</text>
									<text>Tuesday|3</text>
									<text>Wednesday|4</text>
									<text>Thursday|5</text>
									<text>Friday|6</text>
									<text>Saturday|7</text>
								</textlist>
							</keywords>
						</field>
					</par>
				</tablecell>
			</tablerow>
			<tablerow>
				<tablecell valign='center' borderwidth='0px'>
					<pardef id='8' keepwithnext='true' keeptogether='true'>
						<code event='hidewhen'><formula>frequency != "Monthly"</formula></code>
					</pardef>
					<par def='8'>Day of month:</par>
				</tablecell>
				<tablecell valign='center' borderwidth='0px'>
					<pardef id='9' keepwithnext='true' keeptogether='true'>
						<code event='hidewhen'><formula>frequency != "Monthly"</formula></code>
					</pardef>
					<par def='9'>
						<field type='number' kind='editable' name='monthdayToRun'>
							<numberformat format='general' digits='2' punctuated='false' parens='false' percent='false'
							bytes='false'/>
							<code event='defaultvalue'><formula>1</formula></code>
							<code event='inputvalidation'><formula>@If ( frequency != "Monthly"; @Success; @ThisValue > 1 
								& @ThisValue < 29; @Success; @Failure ( "Must be in the first 28 days of the month"))</formula>
							</code>
						</field>
					</par>
				</tablecell>
			</tablerow>
		</table>
		<pardef id='10' keepwithnext='true' keeptogether='true'>
			<code event='hidewhen'><formula>frequency != "Monthly"</formula></code>
		</pardef>
		<par def='10'><run><font size='1pt'/></run></par>
	</tablecell>
</tablerow>

As I use the source view more in XPages, I get more and more comfortable with just editing code, and checking appearances occasionally. While I have only done a little of that in forms, I have used it several times in views. When I created this form, my initial design of it was done by creating a single in the normal designer form, then saving it, and re-opening it in DXL. Then I added several fields to a form with cut-and-paste for field names. Using the properties boxes just seemed like it would take so much longer – after all, I had the field names in my notepad already.

The Agent

Our agent is set to run hourly, on every server. If there are no autoNotify documents, it doesn’t do anything, but if there are, it checks each one for whether it runs on that server, on that day and at that hour.

Sub Initialize
Dim session As New NotesSession
' thisdb is declared in my utilities library, so not declared here '
Dim autoNotifyView As NotesView
Dim autoNotifyDoc As NotesDocument
Dim serverToRunOn As Variant
Dim hourToRun As Variant
Dim frequency As Variant
Dim weekdayToRun As Variant
Dim monthdayToRun As Variant
Dim noticeName As Variant
Dim hourNow As Integer
Dim weekdayToday As Integer
Dim monthdayToday As Integer
Dim reason As String

On Error GoTo errorhandler

Set thisdb = session.Currentdatabase
Call StartAgentLogging ( session )

Dim serverName As New NotesName ( thisdb.Server )

' get view of autonotify documents '
Set autoNotifyView = thisdb.Getview("AutoNotify")
Set autoNotifyDoc = autoNotifyView.Getfirstdocument()

While Not autoNotifyDoc Is Nothing
	' check server to run on '
	serverToRunOn = autoNotifyDoc.Getitemvalue("serverToRunOn")
	If ( Ucase ( serverToRunOn (0) ) = Ucase ( serverName.Common ) ) Then
		' check frequency and day '
		frequency = autoNotifyDoc.Getitemvalue("frequency")
		weekdayToRun = autoNotifyDoc.Getitemvalue("weekdayToRun")
		If ( weekdayToRun (0) = "" ) Then
			weekdayToRun (0) = "0"
		End If
		weekdayToday = Weekday ( Today )
		monthdayToRun = autoNotifyDoc.Getitemvalue("monthdayToRun")
		monthdayToday = Day ( Today )
		If ( frequency (0) = "Daily" or ( frequency (0) = "Weekly" And CInt (weekdayToRun (0)) = weekdayToday ) Or ( frequency (0) = "Weekly" And CInt ( monthdayToRun (0) ) = monthdayToday ) ) Then
			' check hour to run '
			hourToRun = autoNotifyDoc.Getitemvalue("schedule")
			hourNow = Hour (Now)
			If ( CInt ( hourToRun (0) ) = hourNow ) Then
				noticeName = autoNotifyDoc.Getitemvalue("NoticeName")
				If ( sendNotices ( autoNotifyDoc ) ) Then
					Call agentLog.LogAction ( noticeName (0) & " sent")
				Else
					Call agentLog.LogAction ( noticeName (0) & " FAILED")
				End If
			End If
		End If
	End If

	Set autoNotifyDoc = autoNotifyView.Getnextdocument(autoNotifyDoc)
Wend
Call agentLog.LogAction ( "Completed" )

exiting:
	Exit Sub
errorhandler:' report all errors in a messagebox '
	reason = "Error #" & CStr (Err) & " (" & Error & ") on line " & CStr (Erl)
	MessageBox reason, 16, "Error"
	Call agentLog.LogAction ( reason )
	Resume exiting ' transfers control to the exiting label

End Sub

The actual notification builds off the values from the configuration document. At MWLUG, speakers recommended making sure to use functions instead of subroutines, partly because functions return a value and partly for forward compatible with other programming languages. So, my sendNotices function is a boolean, indicating success or failure.

The simplest, yet most powerful part of the script is the application of the querystring. By using that, I could create dozens of notifications from a single view, saving myself disk space by avoiding unnecessary view indices.

Ariwan’s great contribution to the basic agent that made it so useful in this configurable design was the use of columnvalues. The agent simply spits out the contents of the view, populating the message with the details of the document regardless of which fields are used. I’d never thought of doing that!

You’ll notice that in the loop, we get a handle to the nextdoc before processing. If the document would be removed from the view by marking one of the fields “Yes” and saving the document, we need to already have a handle to the next document. If we don’t do that, the view won’t be able to find the next document by referring to the current document, as it has no position in the view any more.

Now, since I want each notification to be processed even if I encounter some errors, I added error-handling in the function as well. If I had not, an error would bubble up to the Initialize routine and stop my agent. This way, it only stops that particular notification, but continues to the next one.

Function sendNotices ( autoNotifyDoc As NotesDocument ) As Boolean
Dim viewName As Variant
Dim recipientGroup As Variant
Dim subjectLine As Variant
Dim introText As Variant
Dim queryString As Variant
Dim flagField As Variant

Dim workingView As NotesView
Dim workingCollection As NotesDocumentCollection
Dim doc As NotesDocument
Dim nextdoc As NotesDocument
Dim memo As NotesDocument
Dim body As NotesRichTextItem
Dim reason As String
Dim count As Integer

sendNotices = false

' get viewName '
viewName = autoNotifyDoc.Getitemvalue( "viewName" )
Set workingView = thisdb.Getview ( viewName (0) )
' apply query string, if there is one '
queryString = autoNotifyDoc.Getitemvalue( "queryString" )
If ( queryString (0)<> "" ) Then
	Call workingView.Ftsearch(queryString(0), 0)
End If

Set doc = workingView.Getfirstdocument()

count = 0

Set memo = thisdb.Createdocument()
Set body = memo.Createrichtextitem(&quot;Body&quot;)
memo.Principal = thisdb.Title

' copy the introductory text from the autoNotify document into the email '
introText = autoNotifyDoc.Getitemvalue( "introText" )
Call body.Appendtext ( introText(0) )
Call body.Addnewline(2)

While Not doc Is Nothing
	Set nextdoc = workingView.Getnextdocument(doc)
	count = count + 1
	Call body.Appendtext( CStr ( count ) & "." )
	Call body.Addtab(1)
	ForAll thing In doc.Columnvalues
		If ( IsArray ( thing ) ) Then
			Call body.Appendtext( Implode (thing, ", " ) )
		Else
			Call body.Appendtext( thing )    
		End If
		Call body.Addtab(1)
	End ForAll
	Call body.Appenddoclink(doc, "Open the doc", "Link")
	Call body.Addnewline(1)

	' if field to mark, then modify field and save doc '
	flagField = autoNotifyDoc.Getitemvalue( "flagField" )
	If ( Trim ( flagField (0) ) <> "" ) Then
		Call agentLog.LogAction ( flagField (0) & " field #" & CStr ( count ) )
		Call doc.ReplaceItemValue ( flagField (0), "Yes" )
		Call doc.Save ( True, False )
	End If

	Set doc = nextdoc

Wend

subjectLine = autoNotifyDoc.Getitemvalue( "subjectLine" )
memo.Subject = CStr (count) & " " & subjectLine (0)
recipientGroup = autoNotifyDoc.Getitemvalue("recipientGroup" )
Call memo.Send(False, DetermineKeyword ( recipientGroup(0)) )

sendNotices = True

exiting:
	Exit Function
errorhandler:' report all errors in a messagebox '
	reason = "Error #" & CStr (Err) & " (" & Error & ") on line " & CStr (Erl)
	MessageBox reason, 16, "Error"
	Call agentLog.LogAction ( reason )
	Resume exiting ' transfers control to the exiting label '

End Function

It’s not quite perfect because if the server is down, it won’t run the notification later. I might take that into account in a future version, since many our project servers are in locations where power may not always be 24×7. Similarly, if someone puts too many notifications to run at the same time, the agent could time out, failing to run all of them.

Hopefully, this exercise proves useful to someone else. I can’t believe I spent more than a decade constantly re-writing the same code when I could have saved myself considerable time by just creating a customizable, reusable piece of code back in the day. Live and learn!

Update:

ThingFailsTurns out there was a bug in the code. The simple loop through the columnValues didn’t take into account multi-value fields. So, when the agent ran on a view with a document that had multiple values, it was trying to print a variant as text. So, I added a simple check for IsArray and imploded the multi-value field to build a comma-delimited string. That avoids the type mismatch that our script was throwing when it found those multi-value fields as shown at right in the debugger.

Advertisement
Categories: Old Notes, Utilities | Tags: , , , , | 1 Comment

Post navigation

One thought on “Configurable notification agent in #OldNotes

  1. Hey David, good idea. You can also write script to sign agents so if you had a database with an approval process for a scheduled agent created by someone else you could review and then have code that not only signs the agent but also enables it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: