Posts Tagged With: calendaring

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

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

Blog at WordPress.com.

%d bloggers like this: