Old Notes

An agent to change field values to help in #XPages

With our transition to XPages, I’ve been finding more and more often that I don’t have a form interface in the Notes client to just change one field value on the back end. Too often, on the front end, in XPages, there’s a value I simply haven’t exposed for editing or don’t even display. I often don’t add the fields to the back end Notes form because it really doesn’t add much value. So, when I want to change one field, or change one field on multiple documents, I do it with an agent. Heck, over the years, we all have. We just usually did it in a very static manner – writing a quick @formula to change the value of a specific field. That requires changing the design to create the agent and then delete the agent – or leaving a mess behind that slowly grows your agent list with more and more single-use agents.

So, I wrote an agent that allowed me to replace a text field by naming the field and the value. Then, I found I wanted one for numbers as well. So, for a few weeks, I had two agents. Then, I realized the folly and wrote an agent that detects the field being updated (or asking you if the field doesn’t exist). I only did it for text, numbers and dates, so I imagine it could be extended. It’s enough for me. Since I had occasion to share it this week with another developer, I thought I’d also post it here to share via the blogosphere.

While there’s nothing brilliant about it, it sure is useful.

%REM
	Agent Change Field Value
	Created Jun 19, 2015 by David Navarre/DAI
	Description: This Agent allows the user to name a field and change the value
		It checks the field type on the first document selected and
		handles strings, numbers and dates differently
%END REM
Option Public
Option Declare
Use "Utilities"
Dim ws As NotesUIWorkspace
Dim newDate As NotesDateTime
Dim newvalue As Variant
Dim fieldname As Variant
Dim change As String
Sub Initialize
	Dim session As New NotesSession
	' thisdb declared in Utilities '
	Dim ndc As NotesDocumentCollection
	Dim itemdoc As NotesDocument
	Dim itemToChange As NotesItem
	Dim numericValue As Double
	Dim itemType As Long
	Dim selectedType (2) As String
	Dim choice As Variant
	Dim reason As String

	On Error GoTo errorhandler

	Set ws = New NotesUIWorkspace
	Set thisdb = session.CurrentDatabase

	selectedType (0) = "Date"
	selectedType (1) = "Text"
	selectedType (2) = "Numbers"

	Call StartAgentLogging (session )

	fieldname = ws.Prompt ( PROMPT_OKCANCELEDIT, "Field Name", "Enter the name of the field to change" )
	If IsEmpty ( fieldname ) Then
		Exit Sub
	End If

	newvalue = ws.Prompt ( PROMPT_OKCANCELEDIT, fieldname, "Enter the new value for " & fieldname )
	If IsEmpty ( newvalue ) Then
		Exit Sub
	End If

	' get the collection before issuing the confirmation, so we can determine field type '
	' from the first document selected, assuming it is the same on the rest '
	Set ndc = thisdb.UnprocessedDocuments
	Set itemdoc = ndc.GetFirstDocument
	Call agentLog.LogAction ( "Items: " & ndc.Count )

	If ( itemdoc.Hasitem(fieldname) ) Then
		Set itemToChange = itemdoc.Getfirstitem(fieldname)
		itemType = itemToChange.Type
	Else
		choice = ws.Prompt(PROMPT_OKCANCELLIST, "Select field type", "Field " & fieldname & " does not exist on the first document. Select field type to create", "Text", selectedType )
		If IsEmpty ( choice ) Then
			MessageBox "Action cancelled"
			Exit Sub
		End If
		Select Case choice
		Case "Date"
			itemType = 1024
		Case "Text"
			itemType = 1280
		Case "Number"
			itemType = 768
		End Select
	End If	

	If Confirm ( itemType ) Then
		While Not itemdoc Is Nothing
			Select Case itemType
			Case 1024  ' DATETIMES '
				Call itemdoc.ReplaceItemValue ( fieldname, newDate )
			Case 1280  ' TEXT '
				Call itemdoc.ReplaceItemValue ( fieldname, newValue )
			Case 768  ' NUMBERS '
				' if the value supplied is an integer, save it that way '
				If ( CInt ( CDbl ( newValue ) ) = CInt ( newValue ) ) Then
					Call itemdoc.ReplaceItemValue ( fieldname, CInt ( newValue ) )
				else
					Call itemdoc.ReplaceItemValue ( fieldname, CDbl ( newValue ) )
				End If
			End Select

			Call agentLog.LogAction ( change )
			Call itemdoc.Save ( True, False )

			Set itemdoc = ndc.GetNextDocument ( itemdoc )
		Wend
		MessageBox change & Chr$(10) & "Successful on " & ndc.Count & " documents"
	End If

exiting:
	Call agentLog.LogAction ( "-------" )
	Call agentLog.LogAction ( "-------" )
	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

End Sub
%REM
	Function ConfirmValue
	Description: This function displays a confirmation dialog based on the field type
%END REM
Function Confirm ( itemType As Long ) As Boolean

	Select Case itemType
		Case 1024  ' DATETIMES '
			Set newDate = New NotesDateTime ( newValue )
			change = "Change date in " & fieldname & " to " & newDate.Dateonly
		Case 1280  ' TEXT '
			change = "Change text in " & fieldname & " to " & newValue
		Case 768  ' NUMBERS '
			change = "Change number in " & fieldname & " to " & newValue
		Case 1  ' RICHTEXT '
			Confirm = False
			MessageBox "Cannot change rich text using this agent"
			Exit Function
		Case Else
			Confirm = False
			MessageBox "Cannot change " & fieldname & " using this agent" & Chr$(10) & "Field type: " & itemType
			Exit Function
	End Select 

	Confirm = ws.Prompt ( PROMPT_YESNO, "Confirmation", change & "?" )

End Function

Oh, and the relevant snippet of the Utilities script library….

%REM
    Library Utilities
    Created Mar 29, 2012 by David Navarre/DAI
    Description: Some database utilities
%END REM
Option Public
Option Declare

Dim thisdb As NotesDatabase
Dim agentLog As NotesLog
Sub Initialize

End Sub

Sub StartAgentLogging ( session As NotesSession )
    ' this module starts agent logging '
    ' 29 Mar 12, David Navarre '
    Dim title As String
    Dim agent As NotesAgent

    Set agentLog = session.CreateLog ("Agent log")
    Set agent = session.Currentagent
    Call agentLog.OpenAgentLog
    Call agentLog.LogAction ( "Log Open" )

End Sub
Categories: Old Notes, Utilities | Tags: , , , , , | 2 Comments

Creating a meeting in the UI in #IBMNotes

When we tout the advantages of using Notes, one of the key points we always mention is the tight integration between Notes applications and Notes mail. Part of this ought to be tight integration with calendaring and scheduling. Oddly, I’ve never written an application that has any interface with the user’s calendar. This had to change. Our users wanted to be able to create a meeting notice from the Quarterly Project Review (QPR) document for that review, taking up the dates and participants from that document seamlessly into the notice. I scratched my head because I’d never even tried it, though I was sure it had to be easy.

So, I looked at what form was used by the meetings I attend. I must have clicked on a proposed meeting because I chose ‘Notice’. I just added a button on the QPR form to create and send Notice documents to the chair and each of the attendees. That looked like it worked because it would show up in people’s inboxes and they could click to accept or decline. Of course, it would disappear once that happened. Oops.

Not only that, but since many of the legacy Notes apps here use formula language to create messages in the UI for users to complete when requesting approvals, my users told me they really wanted to be able to edit the notice. I groused because I find that process inefficient. Users can choose to never send the email or change it in ways that are unexpected. I like approval requests to go silently or to allow the user to enter some additional text, but not to give them full control.

Fortunately, I figured out that I ought to using the Appointment form.

Determining the solution

As I learned more about their requirements, I realized that with all the things they wanted to be able to change, I should give in and simply open in the UI. The fact that any form in the mail template is endlessly complicated was a big incentive as well.

So, I searched the internet to see if anyone else had done this and only came up with a formula language method, which mostly worked, but not quite. Then, taking the formula language code as my example, I built a LotusScript agent that does a nice job of it.

I thought that I could first create the document as a NotesDocument object and then open it using the editDocument method of the NotesUIWorkspace object. I’m not sure if it was because I didn’t set the right fields or not enough fields, but it simply didn’t work when I tried it that way. So, I went the full monty and simply opened it as a NotesUIDocument right from the start.

Interestingly, when all I did was insert names into the required (EnterSendTo) and optional (EnterCopyTo) fields, we realized that you didn’t get to see their schedules to find the right time for the meeting. One of the testers found that clicking on the highlighted ‘Required’ would make them appear. So, checking that link reveals some curious formula language coding:

FIELD EnterSendTo:= @Trim(EnterSendTo);
FIELD EnterCopyTo:= @Trim(EnterCopyTo);
FIELD EnterBlindCopyTo:= @Trim(EnterBlindCopyTo);
@Command([MailAddress];"EnterSendTo";"EnterCopyTo";"EnterBlindCopyTo");
@Command([EditGotoField]; "EnterSendTo");
@Command([EditInsertText]; " ");
@Command([EditGotoField]; "EnterCopyTo");
@Command([EditInsertText]; " ");
@If(EnterBlindCopyTo!="" & @GetProfileField("CalendarProfile"; "showCalBCC") = "1";@Command([EditGotoField]; "EnterBlindCopyTo");"");
@If(EnterBlindCopyTo!="" & @GetProfileField("CalendarProfile"; "showCalBCC") = "1";@Command([EditInsertText]; " ");"");
@PostedCommand([ViewRefreshFields])

So, it’s quirky. Using @Trim, I can understand, but why would it insert the blank space into the two fields? Then I noticed an event on each field.

Sub Onchange(Source As Field)
	Call csEventObj.onChange(FIELD_INVITEES_CHANGED, ITEM_REQUIRED )
	Call csEventObj.UpdateScheduler( ITEM_REQUIRED, ROLE_REQUIRED, APPFLAG_NEW )
	
	If Not (cseventobj.m_note.IsNewNote) Then
		cseventobj.NeedsOLPTran = True
	End If
End Sub

So, the link runs formula language that kicks off the onChange event, which does additional processing. So, when coding one’s agent to create the appointment in the UI, just repeat what the formula language does, only in script. Thus, lines 90-94 in my agent make sense.

The final quirk is with my original QPR document. If the user was in read mode, the agent ran beautifully, but if they were in edit mode, I needed to make sure I had the values on the back end AND that the code didn’t ‘get confused’ with which uidoc was which. Perhaps it was something convoluted with my code, but I found it best if I put the QPR document back into read mode. Then, to avoid issues with how I’m recording the ‘history’ (noting on the QPR that someone created a meeting notice), I decided to close and reopen it in read only mode. Allowing it to be edited was creating confusion, since it threw odd prompts and might generate rep-save conflicts. As such, I’m doing an odd dance with values and objects on lines 50-55.

The agent

%REM
	Agent Send Calendar Invites
	Created Dec 23, 2014 by David Navarre/DAI
	Description: This Agent creates a calendar invite, listing participants and optional participants
%END REM
Option Public
Option Declare
Use "Utilities"
Sub Initialize
	Dim session As New NotesSession
	Dim ws As New NotesUIWorkspace
	Dim thisdb As NotesDatabase
	Dim maildb As New NotesDatabase ( "", "" )
	Dim uidoc As NotesUIDocument
	Dim memoUIdoc As NotesUIDocument
	Dim qprDoc As NotesDocument
	Dim history As NotesRichTextItem
	Dim recipientName As NotesName
	Dim qprDate As Variant
	Dim qprTime As Variant
	Dim projectName As Variant
	Dim fiscalYearAndQuarter As Variant
	Dim participants As Variant
	Dim participantsOptional As Variant
	Dim timeString, dateString As Variant
	Dim answer As Variant
	Dim reason As String
	Dim unid As String

	On Error GoTo errorhandler
	
	Call StartAgentLogging ( session )

	Set thisdb = session.Currentdatabase

	Set uidoc = ws.Currentdocument

	reason = "This will create a meeting invite for you to send to participants."
	If uidoc.Editmode Then
		reason = reason + Chr$(10) + "The QPR will switch to read-only mode."
		reason = reason + Chr$(10) + "If you close and re-open it, you can edit it again."
	End If
	reason = reason + Chr$(10) + "Continue?"
	answer = ws.Prompt ( PROMPT_YESNO, "Continue?", reason )
	If answer = 0 Then
		Exit Sub
	End If	
		
	If uidoc.Editmode Then
		Call uidoc.Save()
		uidoc.Editmode = False
		Set qprDoc = uidoc.Document
		unid = qprDoc.Universalid
		Call uidoc.Close(True)
		Set qprDoc = thisdb.Getdocumentbyunid(unid)
		Set uidoc = ws.Editdocument(False, qprDoc, True)
	Else
		Set qprDoc = uidoc.Document
	End If
	
	Set qprDate = qprDoc.Getfirstitem("QPRDate")
	Set qprTime = qprDoc.Getfirstitem("QPRTime")
	timeString = qprTime.Text
	dateString = qprDate.Text
	Dim qprStartTime As New NotesDateTime ( timeString )

	Call maildb.Openmail()
	Set memoUIdoc = ws.Composedocument(maildb.Server, maildb.Filepath, "Appointment")
	projectName = qprDoc.Getitemvalue("ProjectName") 
	fiscalYearAndQuarter = qprDoc.Getitemvalue("FiscalYearAndQuarter") 
	Call memoUIdoc.Fieldsettext("Subject", fiscalYearAndQuarter(0) & " QPR: " & projectName (0) )

	Call memoUIdoc.Fieldsettext("STARTDATE", dateString )
	Call memoUIdoc.Fieldsettext("STARTTIME", timeString )
	Call memoUIdoc.Fieldsettext("ENDDATE", dateString )
	Call qprStartTime.Adjusthour(1, False)
	Call memoUIdoc.Fieldsettext("ENDTIME", qprStartTime.Timeonly)

	participants = qprDoc.Getitemvalue ( "Participants" )
	ForAll entry In participants
		Set recipientName = New NotesName ( entry )
		Call memoUIdoc.Fieldappendtext("EnterSendTo", recipientName.Abbreviated & Chr$(10) ) 
	End ForAll
	participantsOptional = qprDoc.Getitemvalue ( "ParticipantsOptional" )
	ForAll entry In participantsOptional
		Set recipientName = New NotesName ( entry )
		Call memoUIdoc.Fieldappendtext("EnterCopyTo", recipientName.Abbreviated & Chr$(10) ) 
	End ForAll

	Call memoUIdoc.Gotofield("EnterSendTo")
	Call memoUIdoc.Inserttext(" ")
	Call memoUIdoc.Gotofield("EnterCopyTo")
	Call memoUIdoc.Inserttext(" ")
	Call memoUIdoc.Gotonextfield()
		
	Set history = qprDoc.Getfirstitem("History")
	Call history.Appendtext(Now & " - Meeting notice created by " & session.Commonusername)
	Call history.Addnewline(1, True)
	Call qprDoc.Replaceitemvalue("NoticeFlag", 1)
	Call qprDoc.Save(True, False)
	
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

Final thoughts

I’m sure I can do this a bit more efficiently, but I’m pretty happy with this first foray into calendaring & scheduling. We’ll probably refine this a little and do more of it in our projects. Users always want to be able to skip re-typing everything and there’s no reason not to handle it for them. Of course, we’re likely to have to revise all of this once we move to Verse, but, as my father always said, “I’ll burn that bridge when I come to it.”

Categories: Old Notes | Tags: , , , | Leave a comment

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.

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

Showing a database in a panel in #IBMNotes

I’d never understood the whole ‘widgets’ thing that got added to Notes, where you could have additional things in that right-side set of panels. At first, I put Linked In over there, thinking I’d use it to look up people and keep track of discussions. I never did use it and eventually, it would just show me a login box, so when I changed laptops, I didn’t bother putting it back. I tried some RSS feeds, but they were far too chatty for me to ever keep track. However, I found a great use for them this past month: quick access to back-end of XPages databases.

In our environment, we put all the XPages and custom controls into one ‘design’ database and almost all of the data into another. You guessed it, the almost all is the problem. Some of the configuration documents have to reside in the design database, so it knows where the data resides. I want the users to double-click on the icon and simply open the application in XPiNC, rather than accidentally seeing the man behind the curtain. So, I put a link in the home page of our application that opens the views in Notes:

<xp:link escape="true" id="link1" text="Open Notes views" style="position: absolute;z-index:100;top:25px;left:750px;">
	<xp:this.rendered><![CDATA[#{javascript:var roles = context.getUser().getRoles();
	return @IsMember("[WFAdmin]", roles);}]]></xp:this.rendered>
	<xp:this.value><![CDATA[#{javascript:var server:NotesName = session.createName(@Subset ( @DbName(), 1));
		var filepath = database.getFilePath();
		return "Notes://" + server.getCommon() + "/" + filepath + "/TSWFKeywords?OpenView";}]]>
	</xp:this.value>
</xp:link>

That works fine, but you have to navigate back to the home page to get the the link. I could put it everywhere, but as I was looking at the Drag n Drop sidebar per a request from our newly acquired office in the UK, I figured out that creating the XML files and putting them among your widgets was really easy.

Just create a file with an XML extension — it can have any name, so I tend to create them with meaningful names, like “APPS1 Shared Resources Engage.xml” — with a format similar to this:

<?xml version="1.0" encoding="UTF-8"?>
<webcontextConfiguration version="1.1">
<palleteItem contributeTabOnStartup="false" 
contributeToSideshelfOnStartup="true" 
hideThumbnail="false" 
id="APPS1SharedEngage" 
imageUrl="" 
providerId="com.ibm.notes.toolbox.provider.NotesViewPalleteProvider" 
singletonSidebar="false" 
title="ENGAGE SR" 
url="Notes://APPS1/Projects/Philippines/Engage/EngageSharedRes.nsf" 
viewImageUrl="">
<preferences/>
<data TYPE="DEFAULT"/>
</palleteItem>
</webcontextConfiguration>

I know five of the values that you can edit….

Line Meaning
contributeTabOnStartup=”false” If you want it to always open as a regular tab when you open Notes, set this to true. Otherwise, set this to false.
contributeToSideshelfOnStartup=”true” If you want it to always open as a panel on the right, set this to true. If you’d prefer to double-click and have it open in a new window, set this to false.
id=”APPS1SharedEngage” The unique ID for this widget. If you drag-and-drop another XML file with the same ID, it will update the existing one. When you create one using the menus, it supplies a random numeric one, but text also works, so I’d recommend that using meaningful, text ones.
title=”ENGAGE SR” This is what displays on the icon in the sidebar and on the panel header if you load it in a panel.
url=”Notes://APPS1/Projects/Philippines/Engage/EngageSharedRes.nsf” The Notes URL that you want to open. You can specify a view, but when I tried to specify an agent it didn’t execute the agent. I also tried to specify the XPage, but it never loaded. So, either the database or a specific view (with /viewname?OpenView after the filepath)

Then, drag and drop that XML file from your file system onto the My Widgets panel. In the one I have above, it always opens in a panel on startup, so I can access the views quickly and easily. However, we’re going to have 70 of these at any one time once we roll out our design to all of our projects, so I’ll probably not have any load on startup. If you don’t load them, on startup, double-clicking opens them as a new Notes window.

As soon as I finish figuring out how we’re going to configure the Drag n Drop widget, I’ll post on how to do that. While there is documentation on OpenNTF, I think I can provide some more insight and may look at doing some enhancements down the road.

Categories: Old Notes | Tags: , , | Leave a comment

Simple dialog returning a value to your #XPages

Since I’m still new to XPages, I’m always finding things that are a challenge. Most of the time, I don’t know how big a challenge they are because doing things in XPages isn’t the same as doing them in Old Notes.

Business case

On the payment request in our procurement module, the user needs to enter the exchange rate between the local currency and the base currency for approval routing. Sometimes, the payment has already been made in local currency while the exchange rate has changed. They might only know the amounts in the two currencies, but didn’t record the exchange rate at the time.

PaymentRequest

Solution

In discussions, I suggested that it might not be difficult to simply have a popup that allowed the user to enter the amounts and return the value to the payment request in the UI.

Surprisingly to me, I was right. It’s not that hard. My challenge was that I decide to put this exchange rate computer into a custom control so that I’d be able to re-use it. That meant having to figure out how to have the dialog do a partial refresh on the payment request, in order to recompute the total amount in the base currency (USD).

Existing design considerations

Our procurement module was originally designed by the brilliant minds over at Teamwork Solutions, led by Scott Good. It’s very nice, but this wasn’t included in the original requirements gathering. (What percentage of actual requirements do get into the original requirements gathering?)

Each of those line items displayed in the payment request is a separate Notes document, so, in Old Notes terms, this would be like an embedded view, but since we’re in XPages, it’s a repeat. (I am learning to love repeats!) The total payable amounts are ‘computed fields’ in XPages, which is like a computed for display field in Old Notes. As such, they wouldn’t actually save anything to the payment request document. So, we have some non-displayed computed fields with some server-side Javsacript (SSJS) that aggregates all the local amounts from those view entries and puts it into the control on the XPage. As such, when the exchange rate changes, there is a partial refresh.

So, on our exchange rate control, we had a partial refresh for onchange. Simple control, bound to a field on our payment request document.

<xp:inputText id="exchangeRate" value="#{payDoc.Exchange_Rate}" style="text-align:right;width:70.0px;">
	<xp:this.converter>
		<xp:convertNumber type="number"></xp:convertNumber>
	</xp:this.converter>
	<xp:eventHandler event="onchange" submit="true" refreshMode="partial" refreshId="paymentCostInfoPanel">
	</xp:eventHandler>
</xp:inputText>

Design

So, I created my exchangeRateComputer custom control. I decided to make the link part of the custom control so that implementing it on any other XPage would require the least work possible.  I decided to always set the control on the XPage to be named “exchangeRate”, so I don’t have to pass a string with the control name. As an added bonus, I wanted to fill out the local currency field for the user and just let them enter the final base currency amount they’d like to see.

ExchangeRateCalculator

I ran into one problem. I couldn’t get it to do the partial refresh from the exchangeRateComputer custom control. I’d tried some XSP.partialRefreshGet commands but was having problems and realized…. I might want the name of the element to be refreshed to be different on different XPages. Since passing the compositeData value into the XSP.getElementById seemed beyond my capabilities, I decided to simply defer all refreshes to the exchangeRate control itself. So, I ended up adding an onblur partial refresh. When the user clicks on the ‘Apply to payment request button’, it puts focus onto the exchangeRate control, then, closes the dialog. Closing the dialog blurs focus from the exchangeRate control and…. with an added onblur event, performs a partial refresh for me. (The onblur event is identical to the onchange, except for the name.)

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xe="http://www.ibm.com/xsp/coreex">
	<xp:link escape="true" text="Compute Exchange Rate" id="computeLink" 
		style="margin-left:5px;">
		<xp:eventHandler event="onclick" submit="false">
			<xp:this.script><![CDATA[XSP.openDialog('#{id:computeDialog}');]]></xp:this.script>
		</xp:eventHandler>
	</xp:link>

	<xe:dialog id="computeDialog" title="Exchange rate calculator">
		<xp:table>
			<xp:tr>
				<xp:td>
				</xp:td>
				<xp:td>
					<xp:label id="instructions" value="Enter the amounts below to calculate an exchange rate">
					</xp:label>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td style="width:150px;text-align:right;">
					<xp:label id="total_baseCurrencyLabel" value="Total Base Currency">
					</xp:label>
				</xp:td>
				<xp:td>
					<xp:inputText id="total_baseCurrency" style="width:100px;text-align:right;">
						<xp:this.converter>
							<xp:convertNumber type="number"></xp:convertNumber>
						</xp:this.converter>
						<xp:eventHandler event="onchange" submit="true"
							refreshMode="complete">
							<xp:this.action><![CDATA[#{javascript:var base = getComponent("total_baseCurrency").getValue();
var local = getComponent("total_localCurrency").getValue();
rate = local / base;
getComponent("rate").setValue(rate);}]]></xp:this.action>
						</xp:eventHandler>
					</xp:inputText>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td style="text-align:right;">
					<xp:label id="total_localCurrencyLabel" value="Total Local Currency">
					</xp:label>
				</xp:td>
				<xp:td>
					<xp:inputText id="total_localCurrency"
						defaultValue="#{javascript:compositeData.localCurrency}"
						style="width:100px;text-align:right;">
						<xp:this.converter>
							<xp:convertNumber type="number"></xp:convertNumber>
						</xp:this.converter>
						<xp:this.validators>
							<xp:validateLongRange minimum="1"></xp:validateLongRange>
						</xp:this.validators>
						<xp:eventHandler event="onchange" submit="true"
							refreshMode="complete">
							<xp:this.action><![CDATA[#{javascript:var base = getComponent("total_baseCurrency").getValue();
var local = getComponent("total_localCurrency").getValue();
rate = local / base;
getComponent("rate").setValue(rate);}]]></xp:this.action>
						</xp:eventHandler>
					</xp:inputText>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td style="text-align:right;">
					<xp:label id="rateLabel" value="Exchange Rate"></xp:label>
				</xp:td>
				<xp:td>
					<xp:text escape="true" id="rate"></xp:text>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td>
				</xp:td>
				<xp:td>
					<xp:button value="Apply to payment request"
						id="copyButton">
						<xp:eventHandler event="onclick" submit="true"
							refreshMode="complete">
							<xp:this.script><![CDATA[var rate = XSP.getElementById("#{id:rate}"); 
XSP.getElementById("#{id:exchangeRate}").value = rate.innerHTML;
XSP.getElementById("#{id:exchangeRate}").focus();
XSP.closeDialog('#{id:computeDialog}');]]></xp:this.script>
						</xp:eventHandler>
					</xp:button>
					<xp:button value="Cancel" id="cancelButton">
						<xp:eventHandler event="onclick"
							submit="false">
							<xp:this.script><![CDATA[XSP.closeDialog('#{id:computeDialog}');]]></xp:this.script>
						</xp:eventHandler>
					</xp:button>
				</xp:td>
			</xp:tr>
		</xp:table>
	</xe:dialog>
</xp:view>

The last tweak to it is that my computed exchange rate in the dialog is just a computed field. I struggled a little until a dogpile search revealed that I needed to get that value as innerHTML. If you try getValue() on a computed field, you get bupkis, but if you grab the innerHTML, you’ve got the world in your hands.

It feels like that would be easier in Old Notes, but I hardly care any more. I have a new micro-solution in my toolbox and I expect to re-use this not only for other exchange rate computations, but to re-use the dialog and value-passing in many places. It’s all about building up your toolkit, right?

Categories: Client-Side Javascript, Old Notes, Server-Side Javascript, Xpages, XSP Functions | Tags: , , , , , , , , , , | 1 Comment

Simple view without links in #xpages

One of my co-workers, Neil Enet, asked me the other day if it was possible to set up a view but prohibit the users from opening the documents. I tossed out the idea of using an HTML table with all the HTML for the rows and cells being computed in one column of a view.

"<tr><td>" + approverName + "</td><td>" + approvalType +"</td><td>" + description"</td></tr>"

Unfortunately, that’s pretty clunky and not very satisfying. So, as we talked, I said, “Hey, how about you create an XPage with a view and just don’t make any of the columns clickable?” So, he did.

I asked Neil to comment on it, and he had some great thoughts:

My first XPage. I’ve never competed on a race before, but I imagine the feeling of winning one as being pretty similar to seeing this XPage for the first time. And OK, let’s be honest, it’s a pretty simple XPage. “Simple” might be too much of a word, actually. It’s just a view. ONE view.

The process was extremely easy. I just dragged the View Control, linked it to the view in Notes that I wanted, and voilà. I changed the font size and color of each column, and that felt like an even more awesome achievement.

The funny thing is I’m sure that if I never had the need to create a view like this, where users weren’t able to open documents, this wouldn’t have been so fulfilling. I can see myself saying: “Great, I just created a view, and you can’t open any docs. What’s the big deal about THAT?” But it turns out that that’s exactly what I needed. So simple! And to think that Old Notes didn’t allow me to do this, and that I had to go down the “terrifying” XPages way. Ha!

I can’t think of a better way to start playing with XPages. I now know I can do one, and I know I’m being very naive if I say “XPages is a piece of cake”, but there it is on my system, and I’m sure it will not be the only one.

I’m looking forward to creating more complex XPages, and it’s very very exciting. I might just take a picture of my XPage and put it next to my wife’s here on my desk. And when people walk by and ask me what that is, I’ll answer: “That’s the most simple XPage in the world. But you know what? It’s MY simple XPage.”

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

<xp:viewPanel rows="30" id="viewPanel1">
	<xp:this.facets>
		<xp:pager partialRefresh="true" layout="Previous Group Next" xp:key="headerPager" id="pager1">
		</xp:pager>
	</xp:this.facets>
	<xp:this data>
		<xp:dominoView var="view1" viewName="COPApprovals"></xp:dominoView>
	</xp:this.data>
	<xp:viewColumn columnName="$11" id="viewColumn1" style="font-weight:bold;color:rgb(0,0,160)">
	</xp:viewColumn>
	<xp:viewColumn columnName="$10" id="viewColumn2" style="font-weight:bold;color:rgb(0,64,0)">
	</xp:viewColumn>
	<xp:viewcolumn columnName="$12" id="viewColumn3">
	</xp:viewColumn>
</xp:viewPanel>

</xp:view>

Then, to get users to access it from their Old Notes, using an Outline Entry to open the URL….

targetXpage := "internalApprovals.xsp";

server := @Subset ( @DbName; 1 );
path := @Subset ( @DbName; -1 );

fserver := @Name([CN]; server);
fpath := @ReplaceSubstring(path; "\\"; "/");

url := "notes://" + fserver + "/" + fpath + "/" + targetXpage;

url

And now, Neil has developed his first XPage, users will be able to see status on their documents and everyone will be happy. (Well, I’ll be happier once he goes back in and names the columns in the old view, gives better IDs to the XPage viewColumns, puts it all onto an application layout control and makes everything pretty, but, it works!)

So, if you’re still in fear of XPages, you needn’t be. Go forth and be LOST IN XPAGES with the rest of us!

 

Categories: Old Notes, Xpages | Tags: , , | 2 Comments

Preventing users from opening a document in Notes

As we prepare our pilot of our XPage application, I was reminded that sometimes users might find a way to open a document in the Notes client when we don’t really want them to. While the odds are against them getting to a view or to a Notes document, no system is idiot-proof (idiots are absolutely genius sometimes!) So, I dug out an old script that I’d written back during my time at FAA to prevent users from accidentally opening a Notes database when they should be opening it only via the browser.

So, the users in question need to be able to update the documents, but I want them to do it in XPiNC or in the browser. That means, I can’t go using readernames fields to hide the documents from them and I know that hiding views (either via not including them in an outline or by naming convention) isn’t necessarily going to prevent them from opening my views.

Basically, all we do is check for their roles and if they have the right one, we let them in. Otherwise, they get warned off. I could add some script to this to open the document in the proper XPage, but this is a bare-bones version to help you get started if you have this kind of need.

Sub Queryopen(Source As Notesuidocument, Mode As Integer, Isnewdoc As Variant, Continue As Variant)
Dim session As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
Dim formName As Variant
Dim OKtoOpen As Boolean
Dim roles As Variant

Set db = session.CurrentDatabase
roles = db.QueryAccessRoles(session.UserName)

Set doc = Source.Document
formName = doc.GetItemValue ( "Form" )

OKtoOpen = False

Forall URoles In roles
	If Ucase(URoles) = "[ADMIN]" Then
		OKtoOpen = True
	End If
End Forall

If OKtoOpen = False Then
	Continue = False
	Messagebox "You are not authorized to access " & formName(0) & " documents via the Notes Client!",48, "Access Error"
End If
End Sub

While I was at it, I also wrote another version to be used to keep users out if they accidentally opened it on the backup or development servers. (Yes, I know production databases don’t belong on development servers, but it has happened here and, I am sure, other places).

Sub Queryopen(Source As Notesuidocument, Mode As Integer, Isnewdoc As Variant, Continue As Variant)
	Dim ws As New NotesUIWorkspace
	Dim session As New NotesSession
	Dim db As NotesDatabase
	Dim appsdb As NotesDatabase
	Dim doc As NotesDocument
	Dim appsdoc As NotesDocument
	Dim serverName As New NotesName ( "" )
	Dim dontOpenServers (1) As String

	dontOpenServers (0) = "DominoDev"
	dontOpenServers (1) = "Backup"

	Set db = session.CurrentDatabase
	Set serverName = New NotesName ( db.Server )

	Forall badServer In dontOpenServers
		If serverName.Common = badServer Then
			Continue = False
			Messagebox "You are attempting to open this document on " & serverName.Common & Chr$(10) & "Trying to open the document on MAIN",48, "Wrong Server"
			Set doc = Source.Document
			Set appsdb = New NotesDatabase ( "MAIN/COMPANY", db.FilePath )
			' if that didn't open it, try again
			If Not ( appsdb.IsOpen ) Then
				Call appsdb.Open ( "MAIN/COMPANY", db.FilePath )
			End If
			' if that didn't open it, try by replicaID
			If Not ( appsdb.IsOpen ) Then
				Call appsdb.OpenByReplicaID ( "MAIN/COMPANY", db.ReplicaID )
			End If
			If Not ( appsdb.IsOpen ) Then
				Messagebox "Could not open the MAIN/COMPANY replica of the database, trying local replica",48, "Failed"
				Call appsdb.OpenByReplicaID ( "", db.ReplicaID )
			End If
			If ( apps1db.IsOpen ) Then
				Set appsdoc = apps1db.GetDocumentByUNID ( doc.UniversalID )
				If Not ( appsdoc Is Nothing ) Then
					Call ws.EditDocument ( False, appsdoc )
				Else
					Messagebox "Opened the database, but could not open the document",48, "Failed"
				End If
			Else
				Messagebox "Could not open the local replica of the database either",48, "Failed"
			End If
		End If
	End Forall
End Sub

These QueryOpens could be placed on individual forms or on subforms that are on those forms. You could put a version in the PostOpen event of the database script, though you have to keep in mind that the PostOpen doesn’t run if the user opens a document via a document link instead of opening a view or the database itself. Come to think of it, I will be putting a version in the PostOpen to prevent unauthorized users from opening the database, but have it quietly open the XPiNC page I want them to open.

I thought about putting this in my Security category, but it’s not really about security. It’s mostly about making sure the user gets the proper experience, by using production replicas or the correct client.

Hope you found something interesting here!

Categories: Old Notes, Utilities | Tags: , , , , , , | 5 Comments

Copying design elements via script in #XPages

One of my first posts was about copying a view from one database to many using XPages. Well, as I was surfing the help documents to learn more about FTSearch in order to extend the capabilities of my Excel exporting capabilty, I found createNoteCollection.

createNoteCollection can be used to create a collection of all notes in a Notes database. That is, not just Notes documents, but all Notes design elements. Not only that, but you can quickly choose what kind of notes you want in your note collection. While there are boolean parameters for each type of note you might want, I am most intrigued by the combination ones, which allow you to select by category.

selectAllAdminNotes (ACL and replication formulas)
selectAllCodeElements (agents, database script, outlines, script libaries and misc code elemnts)
selectAllDataNotes (documents AND profile documents)
selectAllDesignElements (code, format, index, help, icons and shared fields)
selectAllFormatElements (actions, forms, framesets, image resources, java resources, misc format elements, pages, style sheets and subforms)
selectAllIndexElements (folders, misc index elements, navigators and views)
selectAllNotes (everything)

So, once you create  your note collection, then you can walk the collection using getFirstNoteID and getNextNoteID, accessing each design element (or document). Then, using NotesDatabase.getDocumentByID to get a handle to it as a NotesDocument, so you can use NotesDocument.CopyToDatabase to copy the design element across to the new database.

var db:NotesDatabase=database;
var notecollection:NotesNoteCollection=db.createNoteCollection();
notecollection.selectAllDesignElements();
var noteID:String=notecollection.getFirstNoteID();
var note:NotesDocument;
var nextNote:NotesDocument;
while ( noteID != null ) {
	note=db.getDocumentByID(noteID);
	if ( note != null ) {
		note.copyToDatabase();
	}
	noteID=notecollection.getNextNoteID();
	nextNote=db.getDocumentByID(noteID);
	note.recycle();
	nextNote=note;
}

Now, it is a little blunt to grab all design elements and copy them from one database to another. One problem is that you are certain to have some duplication of design elements and I’m sure that will create problems. I know that I was able to have two identically named views in my original post, so I didn’t copy the view if it already existed in the destination database. So, you might want to create collections in both databases and compare the design notes t0o make sure not to create duplicates (either by deleting the one in the destination database first or simply not copying the new one in).

Categories: Old Notes, Server-Side Javascript, Utilities, Xpages | Tags: , , | Leave a comment

Finding user roles in #XPages

I’ve written a piece before on roles in XPages, but since that dealt with using the ACL to limit access to a page and not about the programmatic use of roles, I wanted to return to the issue.

Back in old Notes, if we wanted to hide something based if the user did not have a certain role in the ACL, we could use a pretty simple formula:

!@IsMember(“[roleNameHere]”; @UserRoles)

Remarkably, it’s not that much harder in XPages, but there are some important wrinkles to be concerned about. As noted previously and by Russ Maher on his blog, you must remember to use your brackets [] and also keep in mind the result you want (true or false). Remember that the XPages formulas are ‘rendered’ formulas, meaning you want to ‘true’ to display the result and ‘false’ to hide it, so you’d use:

@IsMember(“[roleNameHere]”, context.getUser().getRoles());

Here’s the more complete source code for the rendered formula:

<xp:this.rendered><![CDATA[${javascript:@IsMember("[Testing]", context.getUser().getRoles());}]]></xp:this.rendered>

Now, I’m not sure what impact of referencing the user roles that way has on performance, but since I now I’m using them in many rendered formulas all over my application, I decided to compute it once and then reuse it many times. I put a few extra lines into a control that’s on my main application layout control to drop it into a sessionscope variable. I suppose I might shave a millisecond off if I only computed that once per session, but I didn’t go that far.

<xp:this.beforePageLoad>
<![CDATA[${javascript:var roles = context.getUser().getRoles();
sessionScope.userRoles = roles;}]]>
</xp:this.beforePageLoad>

Then, in order to check to see if my user has one of three roles when rendering an item, I could use this code:

<xp:this.rendered><![CDATA[${javascript:var manager = @IsMember("[InventoryMgr]", sessionScope.userRoles);
var viewer = @IsMember("[InventoryViewer]", sessionScope.userRoles);
var grantsManager = @IsMember("[InvMgrGrants]", sessionScope.userRoles);
if ( manager || viewer || grantsManager ) { return true };
return false; }]]>
</xp:this.rendered>

Now, I also found that sometimes I need to determine the user’s role in my Java code. That’s also not that hard, except that vectors are not quite arrays. If there is a single value, it’s not the same as a multiple value vector. I’m not sure if this is a Notes implementation issue or if it’s the way Java always handles vectors. That is, if it’s a single value, it puts our brackets [] around the value, but it does NOT for multiple values. So, when I was using the code written for us, it wasn’t always picking up the roles correctly. Once I simply told it to check both ways, our code worked more cleanly. (The reference to ExtLibUtil comes from the original code, so I didn’t modify it.)

public Vector getCurUserRoles() {
	try {
		curUserRoles = ExtLibUtil.getCurrentDatabase().queryAccessRoles(ExtLibUtil.getCurrentSession().getEffectiveUserName());
	} catch (NotesException e) {
		this.debug("getCurUserRoles ERROR: " + e.getMessage(), "error");
		curUserRoles = new Vector();
	}
	return curUserRoles;
}

public boolean hasRole(String role, String uname) {
	try {
		Vector roles = this.getCurUserRoles();
		if (roles.contains(role))
			return true;
		if (roles.contains("["+role+"]"))
			return true;
		return false;
	} catch (Exception e) {
		this.debug("hasRole ERROR: " + e.getMessage(), "error");
		return false;
	}
 }

Note that the debugging uses Mark Leusink‘s DebugToolbar, which I highly recommend to everyone.

Categories: Java, Old Notes, Security, Xpages | Tags: , , , , , , | 2 Comments

Shared Action Confusion in Notes

My co-worker, Virginia Tauss, just ran across the most annoying thing in an “Old Notes” database. The database has a number of Shared Actions, some of which are used on forms and views that are inherited explicitly from one of our templates. In our system, each of the projects we support gets a database to manage their project, built off a base design, then modified to meet their needs. Thus, the database does not inherit it’s entire design from the template. In fact, there are some design elements that are drawn from a second template (our Field Operations Manual has several forms and views that are used in all databases).

In my recent design changes, I started using Shared Actions in the Field Operations Manual design, for all the reasons everyone likes Shared Actions – code reusability, simplicity and space savings.

Unfortunately, when a form or view inherited from the Field Operations Manual uses a Shared Action, the inheriting database would not display the same action as had been designated in the template.

Shared Actions rely upon something called a ‘share-id’, which has nothing to do with the name of the action, the order in which it appears in the list of actions in the database or the Note ID of the design element. That is to say, unique to each implementation, rendering Shared Actions far less useful in our multi-template inheritance environment.

From the IBM TechNote

This issue is related to how Shared Actions are referenced. They are not referenced by Note ID, by name, or by their order in the Shared Actions list. They are referenced by a property called ‘share-id’. If a Shared Action is copied to another database the share-id for the action is not necessarily preserved. The issue typically occurs after a design element is updated and the design of the database is refreshed.

Needless to say, this was an eye-opener. I’d never had databases using multiple templates before and hadn’t a lot of experience with Shared Actions in templates. One of my friends, Greg Ehrig, was a ski instructor at one point and he always said, “If you aren’t falling down, you aren’t learning.” Notes and XPages give us all plenty of chances to fall down, so plenty of chances to learn!

Categories: Old Notes | Tags: , , , | 2 Comments

Blog at WordPress.com.

%d bloggers like this: