Posts Tagged With: LotusScript

Creating a meeting notice in Outlook from the Notes client

Another task in our migration to Outlook as the mail client is creating Outlook calendar entries and meeting notices directly from the Notes client. Fortunately, it’s been two years since I wrote about how to do this in the UI in Notes, so I don’t feel like that was wasted time. I was exciting to solve the problem and… oddly enough, solving this one was fun as well. It helped that creating an iCal entry is far simpler than the gyrations we had to go through to create one in Notes. As noted previously, there  aren’t a whole lot of required values to generate in order to have an ICS file that you can open in the UI as a meeting notice/calendar invite.

BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20170622T211500
DTEND:20170622T221500
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="Required Person/Company";RSVP=TRUE:mailto:Required_Person@company.com
ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:external_person@SecondCompany.com
SUMMARY:2017 Q2 QPR: Agribusiness Competitiveness Enhancement via file
UID:AC1804D765C782CD8525814500073F3720170620T104326
END:VEVENT
END:VCALENDAR

Now, keep in mind that this iCal file is a mere fragment. If you sent that file to someone, they get the same behaviour you get — it thinks they’re the meeting organizer and doesn’t save it to their calendar unless they send the ‘update’. The key parameter we leave off is that we don’t set METHOD, since setting that to PUBLISH or REQUEST proved problematic in the Outlook client. If we leave it off, Outlook will allow us to treat it like a brand new calendar entry we’ve created, except that the send button will say ‘Send Update’.

So, let’s review those values in our fragment…

Objects

First, the calendar and event objects are encapsulated. Nothing fancy there.

BEGIN:VCALENDAR
BEGIN:VEVENT
END:VEVENT
END:VCALENDAR

Meeting times

Then we have our start and end times, formatted with date first (YYYYMMDD) then a separator (T) and then the time (HHMMSS). You can include time zone information, but we’re creating this in Outlook and allowing the UI to finish everything for us. So, if the user wants to change the time zone, they can do that in Outlook.

DTSTART:20170622T211500
DTEND:20170622T221500

Attendees

The one required value for our needs in the attendees is the mailto value. Without that, it won’t know who to send the invite to and it simply ignores any other item in that list.

ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN="Meeting Chair/Company";RSVP=TRUE:mailto:Meeting_Chair@company.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="Required Person/Company";RSVP=TRUE:mailto:Required_Person@company.com
ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:external_person@SecondCompany.com

ROLE is not required and can be CHAIR, REQ-PARTICIPANT (required participant), OPT-PARTICIPANT (optional participant) or even NON-PARTICIPANT (for FYI only).

PARTSTAT is not required. There are several values for an attendee in a VEVENT for their participant status, but we’re only concerned with two. Either “NEEDS-ACTION” for attendees that we don’t know when we create the meeting notice whether they’ve agreed to attend or “ACCEPTED” which we’d typically only use for the person creating the meeting notice.

CN is, of course, familiar to us as Notes developers, but it applies here to whatever will be displayed as the attendee name. In my experience, Outlook can parse the abbreviated name and display just the attendee’s common name. That might be our Outlook configuration, but I would assume it’s common.

RSVP would be either true or false, indicating whether you want a response from the attendee. In my case, we always want it from the attendees, other than the current user.

Title and description

I got fooled by this one. In my sample ICS files, I thought there was just an odd carriage return, but the DESCRIPTION value is basically the body or details of the event, while the SUMMARY is what appears in the subject line for the meeting.

SUMMARY:2017 Q2 QPR: Agribusiness Competitiveness Enhancement via file

Meeting ID

I’m guessing that Outlook computes the unique meeting ID itself, but in my code, I generate from the Notes document’s unique ID and then, in order to ensure that subsequent meetings concerning the same document get different IDs, I’m appending a creation time-stamp.

UID:AC1804D765C782CD8525814500073F3720170620T104326

So, the agent I wrote that generates the new meeting notice is pretty straight-forward. The getEmailAddress function was described and detailed in a prior blog post and my Utilities script library only provides the logging functions here. Like my mailto agent, this one relies on the creation of a file in the Notes data directory and opening it using a browser.

The agent

First, you can look over the main part of the agent…

%REM
	Agent (Send Calendar Invites)
	Created Jun 20, 2017 by David Navarre/DAI
	Description: This Agent creates a calendar invite, listing participants and optional participants
%END REM
Option Public
Option Declare
Use "Utilities"

Dim session As NotesSession
Sub Initialize
	Dim ws As New NotesUIWorkspace
'	Dim thisdb As NotesDatabase declared in Utilities script library '
	Dim uidoc As NotesUIDocument
	Dim qprdoc As NotesDocument
	Dim history As NotesRichTextItem
	Dim chairName As NotesName
	Dim recipientName As NotesName
	Dim projectName As Variant
	Dim fiscalYearAndQuarter As Variant
	Dim participants As Variant
	Dim participantsOptional As Variant
	Dim subject As String
	Dim answer As Variant
	Dim reason As String
	Dim unid As String
	
	On Error GoTo errorhandler
	
	set session = New NotesSession
	Call StartAgentLogging ( session )

	If ( openAddressBooks () ) Then
		agentLog.Logaction("Address books opened")
	End If

	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
		
	Dim fileName As String
	Dim dataDirectoryPath As String
	Dim url As String
	Dim fileNumber As Integer
	
	fileNumber = 1
	
	dataDirectoryPath = session.Getenvironmentstring("Directory", True)
	fileName = dataDirectoryPath & "\QPRInvite.ics"
	
	Open fileName For Output As fileNumber
	
	Print # fileNumber, {BEGIN:VCALENDAR}
	Print # fileNumber, {BEGIN:VEVENT}
	Print # fileNumber, {DTSTART:} & getMeetingTime ( "Start", qprDoc ) '20170620T211500
	Print # fileNumber, {DTEND:}  & getMeetingTime ( "End", qprDoc ) '20170620T221500

	' Chair '
	Set chairName = New NotesName ( session.Effectiveusername )
	' when you send the invite from Outlook, it makes you the chair '
	' this line is here to show how you would format an attendee line for the chair '
	' Print # fileNumber, {ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN="} & chairName.Abbreviated & {";RSVP=TRUE:mailto:} & getEmailAddress ( chairName.Abbreviated ) '
	' Required participants '
	participants = qprDoc.Getitemvalue ( "Participants" )
	ForAll entry In participants
		Set recipientName = New NotesName ( entry )
		If Not ( chairName.Abbreviated = recipientName.Abbreviated ) Then
			Print # fileNumber, {ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="} & recipientName.Abbreviated & {";RSVP=TRUE:mailto:} & getEmailAddress ( recipientName.Abbreviated )
		End If
	End ForAll
	' Optional participants '
	participantsOptional = qprDoc.Getitemvalue ( "ParticipantsOptional" )
	ForAll entry In participantsOptional
		Set recipientName = New NotesName ( entry )
		Print # fileNumber, {ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="} & recipientName.Abbreviated & {";RSVP=TRUE:mailto:} & getEmailAddress ( recipientName.Abbreviated )
	End ForAll

	projectName = qprDoc.Getitemvalue("ProjectName") 
	fiscalYearAndQuarter = qprDoc.Getitemvalue("FiscalYearAndQuarter") 
	subject = fiscalYearAndQuarter(0) & " QPR: " & projectName (0)
	Print # fileNumber, {DESCRIPTION:} & subject ' this is the body of the message
	Print # fileNumber, {SUMMARY:} & subject ' this is the meeting name

	' assign a unique ID to meeting using the unid of the document with the current date-time appended '
	' in case user creates multiple meetings for the same QPR '
	Print # fileNumber, {UID:} & qprdoc.Universalid & getMeetingTime ( "Now", qprDoc ) 

	Print # fileNumber, {END:VEVENT}
	Print # fileNumber, {END:VCALENDAR}
	
	Close # fileNumber
	
	url = "file:///" & fileName
	Call ws.Urlopen(url)
	
	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

getMeetingTime

The getMeetingTime function just returns the requested date-time in the format YYYYMMDDTHHMMSS, so it can be included in the creation of the ICS file.

%REM
	Function getMeetingTime
	Description: This Function returns a string in the format YYYYMMDDTHHMMSS
		If it is the start time, the values from the source document are used 		-- 20170622T211500
		If it is the end time, it is adjusted one hour later					 	-- 20170622T221500
		If it is the "Now" time, it returns a string for the current date and time	-- 20170620T094326
%END REM
Function getMeetingTime ( startOrEnd As String, qprDoc As NotesDocument ) As String
	Dim thisNotesDateTime As NotesDateTime
	Dim qprDate As Variant
	Dim qprTime As Variant
	Dim timeString, dateString As Variant
	Dim reason As String	

	On Error Goto errorhandler

	Set qprDate = qprDoc.Getfirstitem("QPRDate")
	Set qprTime = qprDoc.Getfirstitem("QPRTime")
	dateString = qprDate.Text
	timeString = qprTime.Text
	Set thisNotesDateTime = New NotesDateTime ( dateString & " " & timeString )
	Select Case startOrEnd
		Case "End"
			Call thisNotesDateTime.AdjustHour (1)
		Case "Now"
			Set thisNotesDateTime = New NotesDateTime ( Now )
		Case else	
			' keep thisNotesDateTime as set on the source document '
	End Select
	dateString = thisNotesDateTime.DateOnly
	timeString = thisNotesDateTime.TimeOnly
	getMeetingTime = CStr ( Year ( dateString ) )
	getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Month ( dateString ) ), 2 )
	getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Day ( dateString ) ), 2 )
	getMeetingTime = getMeetingTime & "T"
	getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Hour ( timeString ) ), 2 )
	getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Minute ( timeString ) ), 2 )
	getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Second ( timeString ) ), 2 )

exiting:
	Call agentLog.LogAction ( "-------" ) 
	Call agentLog.LogAction ( "-------" ) 
	Exit Function
errorhandler:' report all errors in a messagebox '
	reason = "Function getMeetingTime: "
	reason = reason & "Error #" & Cstr (Err) & " (" & Error & ") on line " & Cstr (Erl)
	Messagebox reason, 16, "Error"
	Call agentLog.LogAction ( reason )
	Resume exiting
End Function

While this did take me a few days to sort out, I’m pretty happy with the result. Our configuration has users sharing one “migration” mail file, so that users who are already on Outlook still retain a mail file and can send email. Unfortunately, that means any email from them that we create in the UI is going to have values pointing back to the “migration” mail file. I spent my first few days on this trying to spoof the mail.box by changing Principal, ReplyTo, $InetAddress and Chair when sending via Notes calendaring. While changing Chair did make it appear to come from the current user, it always displayed the email address from the “migration” mail file. It might have been getting caught in our spam filter on the way to Outlook, as my test user on Notes was still receiving the notices. Nonetheless, by switching to using Outlook as the UI, it not only took away that problem, but was far simpler and future-proofed my application. As I look at these tools I’ve created in LotusScript to generate mail messages and calendar entries, I know that it’s but a short step to doing them in server-side Javascript or maybe in Java.

There is hope for the Notes gurus of old. We just have to keep learning!

iCal RFC (documentation?)

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

Getting email addresses from the Notes address book

As we work to get our Notes applications functioning smoothly with our Outlook mail, I’m finding ways to keep the close binding between Notes applications and the user’s mail. It’s so much easier to click a button to generate an email associated to a particular Notes document than to copy-paste a document link.

We’ve got a Notes form for the Quarterly Project Report. Each quarter, various key members on the project are supposed to have a call to review the project. The form itself is ponderous, having something like 700 fields, but (using hide-whens) can be distilled down to a manageable number for the meeting’s agenda. The Notes names of the expected participants are computed from other documents within the database, though the fields are editable. In switching from using Notes mail to generate the meeting notice to Outlook, I ended up switching to using iCal.

It turns out that iCal is a far simpler way to initiate the meeting notice in the UI. All I need is something like this in an ICS file to have it open in my Outlook client as a meeting notice for me to send (as an update, but more on that in another post)

BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20170622T211500
DTEND:20170622T221500
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="David Navarre/Company";RSVP=TRUE:mailto:David_Navarre@company.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="Another Person/Company";RSVP=TRUE:mailto:Another_Person@company.com
ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:external_person@SecondCompany.com
DESCRIPTION: 
SUMMARY:2017 Q2 QPR: Agribusiness Competitiveness Enhancement via file
UID:AC1804D765C782CD8525814500073F37
END:VEVENT
END:VCALENDAR

One of the hurdles is that we need to have email addresses as well as names for all of the attendees. If you leave the mailto blank or put the Notes name there, it ignores it. Since our address book is still available in Notes and contains everyone’s email address, I thought I’d just google how to do it. Sadly, it wasn’t out there, so I took a few minutes and modified some script in the help documentation (Examples: AddressBooks property) to create a function to do the lookup.

%REM
	Function getEmailAddress
	Description: This Function returns a string that is the email address from the address books

	IMPORTANT: all address books are stored in the NotesDatabase list, addressBookList, which must be called BEFORE calling the getEmailAddress function

%END REM
Function getEmailAddress ( recipientName As String ) As String
	Dim view As NotesView
	Dim doc As NotesDocument
	Dim internetAddress As Variant
	Dim found As Boolean
	Dim reason As String	

	On Error Goto errorhandler

	' if already an internet address, just return that value '
	If ( InStr ( recipientName, "@" ) ) Then
		getEmailAddress = recipientName
		Exit function
	End If

	getEmailAddress = ""
	found = False
	
	ForAll addressBook In addressBookList
		' all address books are stored in the NotesDatabase list, addressBookList, which must be called BEFORE calling the getEmailAddress function '
		' check every Domino Directory, until found '
		If ( addressBook.IsPublicAddressBook ) And ( Not found ) Then
			' look up name in the VIMPeople view of address book '
			Set view = addressBook.GetView( "($VIMPeople)" )
			If not ( view Is Nothing ) Then
				Set doc = view.GetDocumentByKey( recipientName )
				' if person is found, get their internet addrress and stop '
				If Not ( doc Is Nothing ) Then
					internetAddress = doc.Getitemvalue("InternetAddress")
					If ( internetAddress (0) <> "" ) Then
						getEmailAddress = internetAddress (0)
						found = True
						Exit ForAll
					End If
				End If
			End If 
		End If
	End ForAll
	' if found is still False, the person was not found '
	If Not found Then
		MessageBox ( "Unable to locate " & recipientName & " in the address book, using " & recipientName & " as their email addresss" )
		getEmailAddress = recipientName
	End If

exiting:
	Exit Function
errorhandler:' report all errors in a messagebox '
	reason = "Function getEmailAddress: "
	reason = reason & "Error #" & Cstr (Err) & " (" & Error & ") on line " & Cstr (Erl)
	Messagebox reason, 16, "Error"
	Resume exiting
End Function

Hope someone else finds this useful….

Addendum

Dim addressBookList List As NotesDatabase

As Ben pointed out, re-opening every address book every time you want the email address is incredibly inefficient. So, declare a global variable for the address books and use the following function to open them. The code in getEmailAddress now reflects this….

%REM
	Function openAddressBooks
	Description: This Function assigns all address books to a NotesDatabase list and opens them
%END REM
Function openAddressBooks ( ) As Boolean
	Dim reason As String	

	On Error Goto errorhandler

	openAddressBooks = False
	
	ForAll addressBook In session.Addressbooks
		' open every Domino Directory '
		If ( addressBook.IsPublicAddressBook ) Then
			Set addressBookList (addressBook.FileName) = addressBook
			Call addressBookList (addressBook.FileName).Open( "", "" )
		End If
	End ForAll
	
	openAddressBooks = True

exiting:
	Call agentLog.LogAction ( "-------" ) 
	Call agentLog.LogAction ( "-------" ) 
	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
End Function
Categories: Old Notes, Utilities | Tags: , , , , , , | 3 Comments

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: , , , | 2 Comments

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

Exception avoided in FTSearchSorted in #XPages

Regular readers will remember that I’ve been playing around with using FTSearchSorted in order to create Excel spreadsheets from Notes databases in XPages. I’d had that in my sample database for a while, but hadn’t used it our production environment yet. I worried that my experience with crashes was going to be repeated and that it would take forever to integrate it into the design. Fortunately, I got over my concern about crashes (that’s what an admin team is for, right?) since my coding and the versioning all make it seem stable. The integration of the changes (modify one form, one button on a custom control and a few lines in a script library) was relatively quick. So, in the spirit of the 442nd, I decided to “Go For Broke”.

Having pushed the changes to my development environment, I set up a new report with a querystring and a sort column. My brilliant code would soon return a report of all contacts with their city = “Bethesda”, sorted by… Position since that was a column in the view and I was being random. It didn’t work. I played around with the querystring and finally decided to use FTSearch without using sort or my sort column. That worked. So, I tried FTSearchSorted, with the column name in parentheses, so it would definitely be string. Then, a number for the column instead. Then, I decided, I wouldn’t provide a sort column at all. That worked. So it wasn’t the method and it wasn’t the values I was passing to it. It just returned a silly null object every time.

So, I turned to Stack Overflow. Sure enough, someone else had the problem. The answer provided (too short in the Stack Overflow admins’ minds) was from Thomas Adrian, “make sure the view is user sortable”. Aha! I went back to my form, on which I had placed useful help text that I didn’t bother to read:

A column may only be used for sorting if it has been designed to allow “Click on column header to sort” on the Sorting tab of the column properties. The relevant options are Ascending, Descending, and Both. Trying to sort a column in an unsupported direction throws an exception.

I even bolded the important part of the text on my form. I hadn’t read it when I picked my sort column, so it threw an exception every time I used one of those unsorted columns.

Since I’ve demonstrated that even I won’t read my help text, I’ve changed the code on the button that allows you to select which column to use as your sort column so that you can’t choose one that isn’t sorted either ascending or descending. It will only list choices that are click to sort one way or the other (or both).

Forall columns In columnArray
	If ( columns.Isresortascending Or columns.Isresortdescending ) Then
		' add each view name to an array if it is eligible for click to sort
		Redim Preserve headerArray ( count )
		headerArray ( count ) = columns.ItemName
		Print headerArray ( count )
		count = count + 1
	End If
End Forall

I’ve added a clearer explanation to Thomas’ answer, but that short sentence was all the answer I needed. Once my expansion of his answer clears the editors, it will look nicer up there, but since you’ve already read this blog post, you won’t need it, eh?

I’ve updated the sample database, so you can download it to see the rest of how it works if the code sample is not enough. I’ll be at MWLUG starting tomorrow afternoon, so make sure to say “Hello”!

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

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

Creating a one-to-one linking in XPages

In our current project, we create Requisitions with one XPage and then create Purchase Orders with another. Each Requisition may result in many Purchase Orders, but a Purchase Order can only contain line items from a single Requisition. Naturally, the customer wants to be able to move seamlessly from the Requisition to any of the corresponding Purchase Orders and vice-versa.

I wasn’t sure how I’d do this in XPages, but knew I could do it for Notes users without a lot of coding and I could even make it look like web link.

One-to-One In Notes

In regular old Notes, this was very simple. On the Purchase Order form, where I wanted my link I typed Open Procurement Request, then highlighted it and clicked from the top-line menu, Create – Hotspot – Action Hotspot. I made two formatting changes (underline and deselecting the border around the hotspot) 1 and switched the action to run LotusScript. Nice, simple code makes that hotspot open the Requisition:

Sub Click(Source As Button)
	Dim session As New NotesSession
	Dim ws As New NotesUIWorkspace
	Dim thisdb As NotesDatabase
	Dim view As NotesView
	Dim uidoc As NotesUIDocument
	Dim thisdoc As NotesDocument
	Dim reqdoc As NotesDocument
	Dim luvalue As Variant

	Set uidoc = ws.CurrentDocument
	Set thisdoc = uidoc.Document
	luvalue = thisdoc.GetItemValue ( "ProcReqDocID" )

	Set thisdb = session.CurrentDatabase
	Set view = thisdb.GetView ( "LUProcByDocID" )
	Set reqdoc = view.GetDocumentByKey ( luvalue(0) )

	Print luvalue (0)
	If Not reqdoc Is Nothing Then
		Call ws.editDocument ( False, reqdoc )
	End If

End Sub

One-to-One In XPages

Having done it in Notes, I had some guidelines for how I wanted to do it in XPages, rather than just shooting in the dark. I also added display of the requisition number, which turned out to take a few lines of code as well.

	<xp:link escape="true" id="requisitionLink" target="_blank">
		<xp:this.rendered><![CDATA[#{javascript:poDoc.getItemValueString("ProcReqDocID") != "";}]]>
		</xp:this.rendered>
		<xp:this.text><![CDATA[#{javascript:
			var reqID = poDoc.getItemValueString("ProcReqDocID");
			var db:NotesDatabase = getDb("tamisDb");
			var reqView:NotesView = db.getView("LUProcByDocID");
			var reqDoc:NotesDocument = reqView.getDocumentByKey(reqID);
			var reqNumber = reqDoc.getItemValueString("TSWFNumber");
			return "Requisition: " + reqNumber; }]]>
		</xp:this.text>
		<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
			<xp:this.action>
				<xp:openPage name="/pro_procurementRequest.xsp" target="openDocument">
					<xp:this.documentId><![CDATA[#{javascript:
						var reqID = poDoc.getItemValueString("ProcReqDocID");;
						var db:NotesDatabase = getDb("tamisDb");
						var reqView:NotesView = db.getView("LUProcByDocID");
						var reqDoc:NotesDocument = reqView.getDocumentByKey(reqID);
						return reqDoc.getUniversalID();}]]>
					</xp:this.documentId>
				</xp:openPage>
			</xp:this.action>
		</xp:eventHandler>
	</xp:link>

While this was somewhat intimidating, it ended up being not that hard. I will admit that it did take me a whole day to figure out, but it gave me the courage to try tackling linking in the opposite direction, our one-to-many link. For that, I had to expand my recently gained knowledge of repeats. More on that later.

My recent knowledge gain for repeats is thanks to TeamStudio’s webinar on mobile applications. Go figure. As I implement the mobile application that the webinar guided me through building, I’ll blog about that as well. For now, go check out the video.

1) Does anyone use those borders around the hotspot anymore? Since it looks so hideously and is therefor unusable shouldn’t it no longer be the default? Shouldn’t it instead default to underlined?

Categories: Server-Side Javascript | Tags: , , | 1 Comment

Blog at WordPress.com.

%d bloggers like this: