понедельник, 26 апреля 2010 г.

Using Google Apps Script

A while ago I was looking at Updating a Google Calendar and Google Site from a Google Spreadsheet (the beginnings of an event booking system). This idea was inspired by Tony Hirst's work on updating a google calendar from a spreadsheet the endpoint being a way to manage a simple event booking system using Google Apps. This all started to unravel as I couldn't find a way to create a custom booking form for each event. Tony suggested that I should look at just using a generic form which was manually updated with new events, the system handling the rest of the booking process. So with that little hurdle out of the way I revisted my script and as well as rewritting most of it I took the concecpt a little further.

So what do we have now? The video below walks through the workflow:


[You might want to enable full screen view to see what is happening]

The core code is at the end of this post and you can access the full spreadsheet and script here (once you open it click File –> Make a copy to allow you to edit). The bulk of this code is actually a reworking of some existing Google Apps Script tutorials:

To use this code yourself some variables need defining (I could have just called these from a sheet but ran out of time). To do this click Tools –>  Scripts –> Script editor… and you'll see the variables at the top. The first time you run the script a security dialog will popup. You will need to 'grant access' for it to work.

Instructions

Once you do this here are some instructions for use (in general yellow fields are for user input):

  1. In the 'Events' sheet enter title, description, dates etc. You can enter as many events as you like.
  2. To make an event public enter the text 'Add' in the action column, then click Booking System –> Process events. This will push it to calendar and site and create a unique sheet for the event.
  3. To allow bookings click on Form –> Edit form and add the event to the drop down using the format 'ID#{theEventNumberUsedOnTheSheet}{theNameOfYourEvent}' – !this format including whitespaces is really important
  4. A limitation of the Google Apps Script is it doesn't yet handle onFormSubmit actions (Google are looking to add this), so for now to process bookings you need to click Booking System –> Process Bookings. This sends an email to your admin to notify them that there is a booking
  5. To approve a booking enter 'Y' in the Action column and again click Booking System –> Process Bookings. (You can approve as many booking as you like in one go). The script will then send a confirmation to the delegate and copy their details to the appropriate event sheet.
  6. When you are ready to send joining instructions you can edit the message in the EmailTemplates sheet (you can also edit the format of the other emails used). When you are ready to send go to the correct Event sheet then click Booking System –> Email joining instructions

Limitations/waiting for Google to fix

Deleting events from calendar and sites is still a manual process (I don't recall anywhere in the API which allows you to do this). You will also see in the code I've commented out a section which would add all delegates to Google Contacts.

The code

001// AppEventManger Script
002// by mhawksey at http://bit.ly/mashe
003 
004// User defined variables
005var BOOKING_FORM_URL = "http://bit.ly/bookingurl"; //your booking form url (I bit.ly'd mine to shorten
006var SITE_DOMAIN = "Your domain"; // your apps domain name
007var SITE_NAME = "Name of your site"; // your apps site name
008 var CALENDAR_EVENTS = "Name of your calendar"; // the name of the calendar to update
009var STATE_MANAGER_EMAIL = "youremail@gmail.com"; //email address for booking notifications
010 
011 // some additional variables to change at your peril
012var COLUMN_ACTION = 8;
013var COLUMN_STATE = 9;
014var COLUMN_BOOKING_ID = 10;
015var COLUMN_COMMENT = 11;
016var COLUMN_EVENT_ID = 2;
017 
018function onOpen() {
019var ss = SpreadsheetApp.getActiveSpreadsheet();
020var menuEntries = [ {name: "Process Events", functionName: "processEvents"}, {name: "Process Bookings", functionName: "onFormSubmit"}, {name: "Email joining instructions", functionName: "sendEmails"} ];
021ss.addMenu("Booking System", menuEntries);
022}
023 
024 function processEvents() {
025//declare vars
026var cal = CalendarApp.openByName(CALENDAR_EVENTS);
027var ss = SpreadsheetApp.getActiveSpreadsheet();
028var dataSheet = ss.getSheetByName("Events"); // ref sheet name (thought I might have ended up with multiple sheets)
029var data = getRowsData(dataSheet);
030var cal = CalendarApp.getDefaultCalendar();
031var site = SitesApp.getSite(SITE_DOMAIN, SITE_NAME); // .getSite(domain, sitename)
032var annPage = site.getAnnouncementsPages();
033// pull data
034for (var i = 0; i < data.length; ++i) {
035var row = data[i];
036row.rowNumber = i + 2;
037if (row.action =="Add"){
038var descText = row.description + "More info: " + BOOKING_FORM_URL;
039cal.createEvent(row.title, row.start, row.stop, {location:row.location, description:descText}); // create calendar event
040var message = "<strong>Start:</strong> " + Utilities.formatDate(row.start, "GMT", "dd/MM/yy HH:mm")
041+ "<br/><strong>Finish:</strong> " + Utilities.formatDate(row.stop, "GMT", "dd/MM/yy HH:mm")
042+ "<br/><strong>Location:</strong> " + row.location
043+ "<br/><strong>Description:</strong> " + row.description
044+ "<br/><a href='" + BOOKING_FORM_URL + "'>Click here to book a place</a>"; // prepare message
045site.createAnnouncement(row.title, message, annPage[0]); // add announcement to site
046var annList = site.getAnnouncements();
047var eventID = annList.length; // get announcement ID
048var eventSheetName = "Event#" + eventID;
049ContactsApp.createContactGroup(eventSheetName); //create a contact group for the event
050dataSheet.getRange(row.rowNumber, 2, 1, 1).setValue(new Date()); // add today's date/time to 'Added Date' column
051// create a new event booking sheet from template
052ss.setActiveSheet(ss.getSheetByName("EventTMP"));
053var nuSheet = ss.duplicateActiveSheet();
054ss.setActiveSheet(nuSheet);
055ss.renameActiveSheet(eventSheetName); // rename using event Id
056// insert data into sheet
057var eventSheet = ss.getSheetByName(eventSheetName);
058eventSheet.getRange(1, 2, 1, 1).setValue(row.numberOfPlaces);
059eventSheet.getRange(1, 3, 1, 1).setValue(row.title);
060eventSheet.getRange(2, 3, 1, 1).setValue(row.location);
061eventSheet.getRange(3, 6, 1, 1).setValue(row.start);
062eventSheet.getRange(3, 8, 1, 1).setValue(row.stop);
063ss.setActiveSheet(ss.getSheetByName("Events")); // switch back to events sheet
064dataSheet.getRange(row.rowNumber, 3, 1, 1).setValue(eventSheetName);
065dataSheet.getRange(row.rowNumber, 1, 1, 1).setValue("Added by "+Session.getUser().getUserLoginId()); //set the fact that we have updated the calendars for this event
066 
067Browser.msgBox(row.title + " has been published to the calendar and website. IMPORTANT: Add this event to the Form to allow delegates to book in.");
068 }
069}
070}
071 
072function onFormSubmit() {
073// This function has been designed for when App Scripts automatically runs when a Form is submitted.
074// For now it has to be manually started. See http://code.google.com/p/google-apps-script-issues/issues/detail?id=4
075var ss = SpreadsheetApp.getActiveSpreadsheet();
076var dataSheet = ss.getSheetByName("Bookings");
077 
078var templateSheet = ss.getSheetByName("EmailTemplates");
079 var emailTemplate = templateSheet.getRange("A7").getValue();
080 
081// Create one JavaScript object per row of data.
082data = getRowsData(dataSheet);
083 
084 for (var i = 0; i < data.length; ++i) {
085// Get a row object
086var row = data[i];
087row.rowNumber = i + 2;
088if (!row.state) { // if no state notify admin of booking
089var emailTemplate = templateSheet.getRange("A7").getValue();
090var emailText = fillInTemplateFromObject(emailTemplate, row);
091var emailSubject = "Booking Approval Request ID: "+ row.rowNumber;
092MailApp.sendEmail(STATE_MANAGER_EMAIL, emailSubject, emailText);
093dataSheet.getRange(row.rowNumber, COLUMN_STATE).setValue("TBC");
094  
095} else if (row.action == "Y") { // if admin have approved send confirmation
096var approvedOrRejected = (row.action == "Y") ? "confirmed" : "rejected";
097  
098// capture the sheet to copy booking to
099var eventID = row.event;
100eventID = eventID.substring(eventID.indexOf("#")+1,eventID.indexOf(" -"));
101var eventSheet = ss.getSheetByName("Event#" + eventID);
102 
103// create a booking ID
104 var bookingID = "ID#"+eventID+"B"+row.rowNumber;
105dataSheet.getRange(row.rowNumber, COLUMN_BOOKING_ID).setValue(bookingID);
106 
107// copy booking details to event sheet
108var rowNum = eventSheet.getLastRow()+1;
109eventSheet.getRange(rowNum, 3, 1, 1).setValue(bookingID);
110eventSheet.getRange(rowNum, 4, 1, 1).setValue(row.timestamp);
111eventSheet.getRange(rowNum, 5, 1, 1).setValue(row.firstName);
112eventSheet.getRange(rowNum, 6, 1, 1).setValue(row.surname);
113eventSheet.getRange(rowNum, 7, 1, 1).setValue(row.email);
114eventSheet.getRange(rowNum, 8, 1, 1).setValue(row.organisation);
115eventSheet.getRange(rowNum, 9, 1, 1).setValue(row.otherInfo);
116eventSheet.getRange(rowNum, 10, 1, 1).setValue(row.comments);
117 
118//Add/edit details of new/existing delegate to Google Contacts
119// This code generates a 'Service error: Contacts : POST method does not support
120// concurrency' looks like an API bug.
122/*
123var curDate = Utilities.formatDate(new Date(), "GMT", "dd/MM/yy HH:mm");
124var c = ContactsApp.findByEmailAddress(row.email);
125if (!c){
126var c = ContactsApp.createContact(row.firstName, row.surname, row.email);
127var prop = {};
128prop.Organisation = row.organisation;
129prop.Added = curDate;
130c.setUserDefinedFields(prop);
131 var group = ContactsApp.findContactGroup(row.organisation);
132if (!group){
133var group = ContactsApp.createContactGroup(row.organisation);
134}
135c.addToGroup(group);
136} else {
137 c.setUserDefinedField("Last activity", curDate);
138}
139//var group = ContactsApp.findContactGroup("EventID#"+eventID);
140//c.addToGroup(group); // add contact to event group
141*/
142//prepare email
143var emailTemplate = templateSheet.getRange("A4").getValue();
144var emailText = fillInTemplateFromObject(emailTemplate, row);
145var emailSubject = "Booking Approved (Booking ID: "+ bookingID + ")";
146// fill in the template using stored variables
147emailText = emailText.replace("STATE_MANAGER_EMAIL", STATE_MANAGER_EMAIL || "");
148emailText = emailText.replace("APPROVED_OR_REJECTED", approvedOrRejected || "");
149emailText = emailText.replace("BOOKING_ID", bookingID || "");
150MailApp.sendEmail(row.email, emailSubject, emailText);
151 
152// Update the state of bookings
153dataSheet.getRange(row.rowNumber, COLUMN_STATE).setValue(STATE_APPROVED+" by "+Session.getUser().getUserLoginId());
154 dataSheet.getRange(row.rowNumber, COLUMN_ACTION).setValue("");
155 }
156}
157}
158 
159// Code to send joining instructions - based on simple mail merge code from
160// Tutorial: Simple Mail Merge
161// Hugo Fierro, Google Spreadsheet Scripts Team
162// March 2009
163function sendEmails() {
164var ss = SpreadsheetApp.getActiveSpreadsheet();
165var dataSheet = ss.getActiveSheet();
166var eventName = ss.getRange("C1").getValue();// pull event name from sheet
167 
168var dataRange = dataSheet.getRange(5, 3, dataSheet.getLastRow() - 3, 8);
169 
170var templateSheet = ss.getSheetByName("EmailTemplates");
171 var emailTemplate = templateSheet.getRange("A10").getValue();
172 
173// Create one JavaScript object per row of data.
174objects = getRowsData(dataSheet,dataRange,4);
175 
176// For every row object, create a personalized email from a template and send
177// it to the appropriate person.
178for (var i = 0; i < objects.length; ++i) {
179// Get a row object
180var rowData = objects[i];
181rowData.rowNumber = i + 5;
182// Generate a personalized email.
183// Given a template string, replace markers (for instance ${"First Name"}) with
184// the corresponding value in a row object (for instance rowData.firstName).
185if (!rowData.emailed) {
186var emailText = fillInTemplateFromObject(emailTemplate, rowData);
187emailText = emailText.replace("EVENT_NAME", eventName);
188var emailSubject = "Joining Instrucations for " + eventName;
189 
190MailApp.sendEmail(rowData.emailAddress, emailSubject, emailText);
191dataSheet.getRange(rowData.rowNumber, 2).setValue(Utilities.formatDate(new Date(), "GMT", "dd/MM/yy HH:mm"));
192}
193}
194}
195  
196// Replaces markers in a template string with values define in a JavaScript data object.
197// Arguments:
198//   - template: string containing markers, for instance ${"Column name"}
199//   - data: JavaScript object with values to that will replace markers. For instance
200//           data.columnName will replace marker ${"Column name"}
201// Returns a string without markers. If no data is found to replace a marker, it is
202// simply removed.
203function fillInTemplateFromObject(template, data) {
204var email = template;
205// Search for all the variables to be replaced, for instance ${"Column name"}
206var templateVars = template.match(/\$\{\"[^\"]+\"\}/g);
207// Replace variables from the template with the actual values from the data object.
208// If no value is available, replace with the empty string.
209for (var i = 0; i < templateVars.length; ++i) {
210// normalizeHeader ignores ${"} so we can call it directly here.
211var variableData = data[normalizeHeader(templateVars[i])];
212email = email.replace(templateVars[i], variableData || "");
213}
214 
215return email;
216}
217 // there are also some Google example functions used getRowsData, getObjects, normalizeHeaders, normalizeHeader, isCellEmpty, isAlnum and isDigit

Комментариев нет:

Отправить комментарий