Apps Script started out as a simple tool to let developers add new features to Google Apps, but it’s grown into a programming platform that thousands of professional coders use every day. We hear a couple common requests from developers when they’re building complex projects with Apps Script: they want a full-featured IDE, and they want to sync to external version-control systems like GitHub.
Today, we’re introducing support for Apps Script in the Google Plugin for Eclipse. You can now sync with your existing Apps Script files on Google Drive, edit them in Eclipse — offline, if necessary, and with all the benefits of autocomplete — then write your code back to Drive so you can run it in the cloud. Because the plugin stores a copy of each script in a local workspace, you can manage Apps Script projects with your favorite version-control system.
Getting started is easy:
For step-by-step instructions on installation and use, see the documentation on using Apps Script with the Google Plugin for Eclipse.
Just in case you were wondering, the plugin uses the public Google Drive SDK to sync Apps Script files between your local file system and Google’s servers. We’ve previously covered the techniques it uses in our documentation on importing and exporting projects and the recent episode of Apps Script Crash Course on Google Developers Live below.
Last month, we announced several new ways to customize Google Forms. As of this week, three of those options are also available in forms created from Apps Script — embedding YouTube videos, displaying a progress bar, and showing a custom message if a form isn’t accepting responses.
Adding a YouTube video is as simple as any other Google Forms operation in Apps Script — from the Form object, just call addVideoItem(), then setVideoUrl(youtubeUrl). Naturally, you can also control the video’s size, alignment, and so forth.
Form
addVideoItem()
setVideoUrl(youtubeUrl)
To show a progress bar, call setProgressBar(enabled). Don’t even need a second sentence to explain that one. The custom message for a form that isn’t accepting responses is similarly easy: setCustomClosedFormMessage(message), and you’re done.
setProgressBar(enabled)
setCustomClosedFormMessage(message)
Want to give it a try yourself? Copy and paste the sample code below into the script editor at script.google.com, then hit Run. When the script finishes, click View > Logs to grab the URL for your new form, or look for it in Google Drive.
script.google.com
function showNewFormsFeatures() { var form = FormApp.create('New Features in Google Forms'); var url = form.getPublishedUrl(); form.addVideoItem() .setVideoUrl('http://www.youtube.com/watch?v=38H7WpsTD0M'); form.addMultipleChoiceItem() .setTitle('Look, a YouTube video! Is that cool, or what?') .setChoiceValues(['Cool', 'What']); form.addPageBreakItem(); form.addCheckboxItem() .setTitle('Progress bars are silly on one-page forms.') .setChoiceValues(['Ah, that explains why the form has two pages.']); form.setProgressBar(true); form.setCustomClosedFormMessage('Too late — this form is closed. Sorry!'); // form.setAcceptingResponses(false); // Uncomment to see custom message. Logger.log('Open this URL to see the form: %s', url); }
Security is a top priority for Google, just as it is for many of our Google Apps customers. As a domain administrator, a big part of keeping your users safe is knowing when and how they are using their accounts. Since we launched the Admin SDK Reports API in June, we've continued to add features to let you more easily visualize Google Apps' usage and security in your domain. These new features include:
Security Reports
Usage Reports
Refer to the Reports API documentation for more details on how to use the Reports API and to see what is possible with all of our entire Admin SDK.
Drive is a great drop zone for incoming files -- no matter if they’re coming from cameras, scanners, faxes, or countless other devices or apps. But throwing files into the root folder makes it difficult for users to find and organize their content.
I’ve seen developers create a folder when the user first connects the app with Drive to keep files organized by the app that created it. It’s a simple technique that is easy to implement and good default behavior for most applications.
With the Picker API, we can go a step further and offer users the choice of destinations too. For example, I’d like my scanner to put work files in one folder and personal files in another, or even configure multiple destinations depending on the type of document I’m scanning.
For this particular use case, the picker needs to be configured to show folders and only show folders. That requires customizing the DocsView just a little.
var docsView = new google.picker.DocsView() .setIncludeFolders(true) .setMimeTypes('application/vnd.google-apps.folder') .setSelectFolderEnabled(true);
By enabling folders & the ability to select them while simultaneously filtering out everything else, users can quickly and easily select one of their existing folders as a destination. The rest of the code to show the picker is par for the course.
// Handle user actions with the picker. var callback = function(data) { if (data.action == google.picker.Action.PICKED) { var doc = data.docs[0]; alert("You picked " + doc.id); } }; var picker = new google.picker.PickerBuilder() .addView(docsView) .setCallback(callback) .build(); picker.setVisible(true);
Not only is this easy to implement, it’s safer for users. By offloading this functionality to the Picker API, an app only needs the drive.file scope to write files into the user’s preferred location.
You can learn more about the Picker API at developers.google.com or ask questions at StackOverflow with the google-drive-sdk tag.
Google+ make it easy for Google Apps customers to connect and share within their organisation and encourage collaboration between teams. Today we’re launching an update to the Google+ Android app that includes a number of new features for Google Apps customers, and a new developer offering, the Google+ Domains API.
The Google+ Domains API allows Google Apps customers to integrate Google+ into their existing tools and processes, and allows enterprise software vendors to access Google+ from their products. Applications using the Google+ Domains API can act on behalf of Google Apps users to share posts within the same domain, comment on posts shared within the domain, and manage Circles. In addition, the Google+ Domains API enables Google Apps domain administrators to pre-populate the Circles of new employees, or review sharing activity.
For example, Ocado is building a tool that uses the Google+ Domains API to regularly sync team membership stored in Active Directory with the circles of their employees. This will ensure that every employee always has an up to date circle containing the other members of their team. Cloudlock is using the Google+ Domains API to add support for Google+ to its suite of data loss prevention, governance, and compliance applications.
Any developer can begin developing with the Google+ Domains API today. However only members of a Google Apps domain can use Google+ Domains API applications. To get started check out the documentation. If you have any questions, you can consult the google-plus tag on Stack Overflow, or join the “Developing with Google+” Google+ Community.
Google Apps Script is, first and foremost, a tool for making Google Apps more powerful — and today’s addition of programmatic control over data-validation rules in Google Sheets is a perfect example. For a quick demo, make a copy of this spreadsheet, then follow the instructions provided.
For the last few months, scriptable access to the data validation feature in Sheets has been the most requested feature on the Apps Script issue tracker. A common use for data-validation rules is to require that a cell’s value match one of the values in a different range. The following example shows how to achieve that goal with Apps Script. First, we use the newDataValidation() method to construct a DataValidationBuilder, then set the appropriate options and apply the final DataValidation with setDataValidation().
newDataValidation()
DataValidationBuilder
DataValidation
setDataValidation()
// Set the data-validation rule for cell A1 to require a value from B1:B10. var cell = SpreadsheetApp.getActive().getRange('A1'); var range = SpreadsheetApp.getActive().getRange('B1:B10'); var rule = SpreadsheetApp.newDataValidation().requireValueInRange(range) .build(); cell.setDataValidation(rule);
It’s also possible to modify existing data-validation rules. The next example changes rules that require a date in 2013 to require a date in 2014 instead. You’ll see that the script calls getDataValidations() to retrieve the existing rules, then uses getCriteriaType(), getCriteriaValues(), and the DataValidationCriteria enum to examine the rules before applying the new date restriction via the advanced withCriteria() method.
getDataValidations()
getCriteriaType()
getCriteriaValues()
DataValidationCriteria
withCriteria()
// Change existing data-validation rules that require a date in 2013 // to require a date in 2014. var oldDates = [new Date('1/1/2013'), new Date('12/31/2013')]; var newDates = [new Date('1/1/2014'), new Date('12/31/2014')]; var sheet = SpreadsheetApp.getActiveSheet(); var range = sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns()); var rules = range.getDataValidations(); for (var i = 0; i < rules.length; i++) { for (var j = 0; j < rules[i].length; j++) { var rule = rules[i][j]; if (rule != null) { var criteria = rule.getCriteriaType(); var args = rule.getCriteriaValues(); if (criteria == SpreadsheetApp.DataValidationCriteria.DATE_BETWEEN && args[0].getTime() == oldDates[0].getTime() && args[1].getTime() == oldDates[1].getTime()) { rules[i][j] = rule.copy().withCriteria(criteria, newDates).build(); } } } } range.setDataValidations(rules);
With this new feature in Apps Script, you should find it much easier to manage complex data-validation scenarios. Please keep the feature requests coming!
For developers, part of the simplicity of Apps Script has always been that authorization requires zero setup — but we heard from users that the process required too many clicks. At Google I/O this year, we launched an opt-in version of an easier authorization flow; today, that new flow becomes the default for all new scripts.
Besides being prettier and easier, the new flow offers benefits behind the scenes: it allows more scripts to be simultaneously authorized on the same account, which means you shouldn’t need to reauthorize a script unless the code changes substantially.
For developers who use the advanced Google services, the new flow also gets rid of some manual steps that were previously required. Every new script now automatically creates a project in the Google APIs Console — no more messing with secret keys!
If you want the same experience for your existing scripts, you can upgrade them manually in just a few seconds.
engineering/backend-support
engineering-backend-support
If your app needs to keep up with changes in Drive, whether to sync files, initiate workflows, or just keep users up to date with the latest info, you’re likely familiar with Drive’s changes feed. But periodic polling for changes has always required a delicate balance between resources and timeliness.
Now there’s a better way. With push notifications for the Drive API, periodic polling is no longer necessary. Your app can subscribe for changes to a user’s drive and get notified whenever changes occur.
Suppose your app is hosted on a server with my-host.com domain and push notifications should be delivered to an HTTPS web-hook https://my-host.com/notification:
my-host.com
https://my-host.com/notification
String subscriptionId = UUID.randomUUID().toString(); Channel request = new Channel() .setId(subscriptionId) .setType("web_hook") .setAddress("https://my-host.com/notification"); drive.changes().watch(request).execute();
As long as the subscription is active, Google Drive will trigger a web-hook callback at https://my-host.com/notification. The app can then query the change feed to catch up from the last synchronization point:
changes = service.changes().list() .setStartChangeId(lastChangeId).execute();
If your app only needs to be notified about changes to a particular file or folder your app can watch just those files rather than the entire change feed.
If you are interested in using this new feature, please refer to the documentation at developers.google.com. You can see push notifications in action with the Push Notifications Playground and view the source at Github.
Many developers have come to prefer JSON for data serialization, but we recognize that good ol' XML is still an important format for many Apps Script users. Our existing XML service is good at parsing XML, but has limited ability to create or alter existing documents. In order to provide a more complete and consistent experience, we have created a new XML service, which launches today. The new service is accessed using XmlService, in contrast to the old service which was simply called Xml.
XmlService
Xml
Let's take a look at how you can use the new service to create an XML representation of the emails in your Gmail inbox.
function createXml() { var root = XmlService.createElement('threads'); var threads = GmailApp.getInboxThreads(); for (var i = 0; i < threads.length; i++) { var child = XmlService.createElement('thread') .setAttribute('messageCount', threads[i].getMessageCount()) .setAttribute('isUnread', threads[i].isUnread()) .setText(threads[i].getFirstMessageSubject()); root.addContent(child); } var document = XmlService.createDocument(root); var xml = XmlService.getPrettyFormat().format(document); Logger.log(xml); }
The code above logs XML that looks something like this:
<?xml version="1.0" encoding="UTF-8"?> <threads> <thread messageCount="1" isUnread="true"> Can't wait for the new XML service! </thread> <thread messageCount="1" isUnread="true"> 50% off all widgets through Friday </thread> <thread messageCount="3" isUnread="false"> Don't forget about the picnic on Saturday </thread> </threads>
The new XML service has some notable advantages over the old service:
CDATA sections
Comments
With the launch of this new service, we are deprecating some of our older XML tools in Apps Script, specifically the old XML service, the SOAP service, and the JavaScript feature E4X. Calls to these services will continue to work, but we encourage you to start migrating your code to the new XML service for better long-term support. On February 1, 2014, these old services will no longer appear in auto-complete or in our documentation, per the Apps Script sunset schedule.
Ever wanted to programmatically insert something at the cursor in Google Docs (say, a “Sign Here” image) or read the user’s selection (maybe for an on-the-spot translation)? Starting today, you can.
Apps Scripts bound to Google Docs can now access the active user's Cursor and Selection by calling Document.getCursor() and Document.getSelection(), respectively. The returned objects provide useful information like the element the cursor is positioned in and an array of all of the elements contained in the selection.
Document.getCursor()
Document.getSelection()
This Google Doc contains a simple script that uses Apps Script’s Language Service to translate selected text from English to Spanish through a custom menu item.
Here, it uses the getSelectedElements() method of the Selection class to get an array of selected elements:
getSelectedElements()
Selection
var selection = DocumentApp.getActiveDocument().getSelection(); if (selection) { var elements = selection.getSelectedElements();
Next, it loops through each element, performs the translation, and replaces the original text:
var translatedText = LanguageApp.translate( element.asText().getText(), 'EN', 'ES'); element.asText().setText(translatedText);
At Google I/O this year, Apps Script engineer Jonathan Rascher demonstrated Bibstro, a bibliography sample app for Google Docs that inserts inline citations at the cursor. Today, we’re releasing the source code for Bibstro; you can also try it out by making of copy of this Google Doc.
To insert text, the script calls the aptly named insertText() method of the Cursor object:
insertText()
Cursor
var cursor = DocumentApp.getActiveDocument().getCursor(); if (cursor) { // Determine the text of the new inline citation to insert. var citation = bibStrategy.getInlineCitationText(...); var surroundingText = cursor.getSurroundingText().getText(); var surroundingTextOffset = cursor.getSurroundingTextOffset(); if (surroundingTextOffset > 0 && surroundingText.charAt(surroundingTextOffset - 1) != ' ') { // If the cursor follows a non-space character, insert a space // and then the citation. cursor.insertText(' ' + citation); } else { // Otherwise, just insert the citation. cursor.insertText(citation); } }
You’ll also notice that the script uses the Cursor class’s getSurroundingText() method to determine whether to insert a space before the new inline citation.
getSurroundingText()
To help you become familiar with how cursor and selection work, we've also created a Cursor Inspector sample script. As you navigate through a document, the script displays up-to-date information about your cursor or selection in a custom sidebar. We’re also releasing the source code for Cursor Inspector on GitHub.
These new APIs are available immediately. We’re excited to see what kind of scripts you come up with!
Flubaroo, a popular Apps Script application that helps teachers with grading, has just reached version 3.0. The new features and improvements include:
If you know any teachers who aren’t using Flubaroo yet, why not encourage them to try it out? It doesn’t cost a thing, and has helped thousands of teachers save time and gain insight into student performance — all through the power of Apps Script.
Ever look at the data returned when using the Drive API? A files.list call, even if just returning a single file, can yield upwards of 4kb of data. Drive has a rich set of metadata about files, but chances are your application only needs a small fraction of what’s available.
files.list
One of the simplest but most effective optimizations you can make when building apps with the Drive API is limiting the amount of data returned to only those fields needed for your particular use case. The fields query parameter gives you that control, and the results can be dramatic.
A simple example of this is using the files.list call to display a list of files to a user. The naive query, https://www.googleapis.com/drive/v2/files?maxResults=100, generated more than 380kb of data when I ran it against my own corpus. But to render this list nicely, an app only needs a few bits of information -- the document title, icon & thumbnail URLs, the mime type, and of course the file ID.
https://www.googleapis.com/drive/v2/files?maxResults=100
Using the fields query parameter, the results can be trimmed to just the necessary fields and those needed for fetching subsequent pages of data. The optimized query is https://www.googleapis.com/drive/v2/files?maxResults=100&fields=items(iconLink%2Cid%2Ckind%2CmimeType%2CthumbnailLink%2Ctitle)%2CnextPageToken.
https://www.googleapis.com/drive/v2/files?maxResults=100&fields=items(iconLink%2Cid%2Ckind%2CmimeType%2CthumbnailLink%2Ctitle)%2CnextPageToken
After modifying the query the resulting data was only 30k. That’s more than a 90% reduction in data size! Besides reducing the amount of data on the wire, these hints also enable us to further optimize how queries are processed. Not only is there less data to send, but also less time spent getting it in the first place.
Editor’s Note: Guest author Niels Buekers is a Google Apps consultant at Capgemini Belgium. — Arun Nagarajan
During a recent Google Apps migration project, we received several requests to create custom groups of contacts so that users could more easily email frequent collaborators. Before switching to Google Apps, users created their own private distribution lists — but this approach led to overlapping groups that quickly fell out of sync.
The problem was a perfect case for Google Apps Script. We built a great solution that gives users as much power as possible with just a quick administrator review.
The situation before: either manually adding each contact or using a private contacts group.
To start the process, a user adds a specific label to a Gmail message. A script that runs on a timed trigger then generates a request to create a group for all the addresses in the message. The script writes this data to a spreadsheet that tracks group names and administrator approval.
/** * Retrieves all 'group_request' threads and creates a request. */ function processInbox() { // Get threads that have the group_request label. var groupRequestLabel = GmailApp.getUserLabelByName('group_request'); var threads = groupRequestLabel.getThreads(0, 10); // For each thread, retrieve all recipients and create a group request. for (var i = 0; i < threads.length; i++) { var firstMessage = threads[i].getMessages()[0]; var sender = firstMessage.getFrom(); var recipients = []; // Add sender. recipients.push(parseAddresses(sender)); // Add recipients. if (threads[i].getMessages()[0].getTo()) { var toRecipients = parseAddresses(firstMessage.getTo()); recipients.push(toRecipients); } // Add CCs. if (threads[i].getMessages()[0].getCc()){ var ccRecipients = parseAddresses(firstMessage.getCc()); recipients.push(ccRecipients); } // Write all recipients to a cell in the spreadsheet // and send emails to ask for group name and approval. createGroupRequestForRecipients(recipients, Session.getActiveUser().getEmail()); // Remove label from this thread now that it has been processed. threads[i].removeLabel(groupRequestLabel); } };
Once the request has been processed and written to the spreadsheet, the script sends the user an email that asks her to suggest a name for the group in an Apps Script web app. A second email asks the administrator to visit the web app to approve or decline the request. The results are again stored in the spreadsheet.
The spreadsheet contains a second script, which is triggered for each modification. Once the script confirms that the request has been approved, it uses the Apps Script Domain Service to create the new group.
/** * Creates a new group in the Google Apps cPanel with the provided name * and members. */ function createGroupWithAddresses(addresses,groupName){ var group = GroupsManager.createGroup(groupName, groupName, groupName, GroupsManager.PermissionLevel.DOMAIN); var splitAddresses = addresses.split(','); for (var i = 0; i < splitAddresses.length; i++) { Logger.log('Adding ' + splitAddresses[i]); group.addMember(splitAddresses[i]); } };
The result after successfully running the script.
This solution provides a simple way for users to request new Google groups, without all the overhead of manually creating an admin-managed distribution list.
Editor’s Note: Guest author Jason Gordon is a co-founder of Beth Macri Designs — Arun Nagarajan
Beth Macri Designs creates jewelry from the point of view of a structural engineer. The forms are designed using generative 3D software systems and materialized using 3D printing technologies. Our company understands that to make beautiful fine jewelry, 3D printing is only the first step; traditional jewelry craft is then employed for final production. After our first product, The Hidden Message Necklace, was recently featured on The View as part of its Valentine's Day Gift Guide, we had a lot of orders to ship out. As soon as the mail leaves the building, though, the process is literally out of our hands: something unexpected was bound to happen to at least one or two packages. Several package-tracking services exist, but getting the names and tracking numbers into them was a cut-and-paste operation.
I knew that all of the tracking numbers were being delivered by email and I had already set up a Gmail filter to archive them and apply a label. With a little help from Google Apps Script, I knew I could automatically parse those emails and add them to my account on PackageTrackr (which syncs to their newer service, Fara).
The script supports reading emails from multiple shipping providers and is set up so one could easily add more. Every 30 minutes on a time-driven trigger, using the Gmail service, the script runs and looks through unread emails from the shipping provider label, then parses the name and tracking number out of each one. The provider, tracking number, and recipient are stored in a JavaScript array.
function getUSPSConversations(){ return GmailApp.search("in:usps is:unread subject:(Click-N-Ship)"); } function matchUSPSHTML(data){ var out = []; var track_num = data.match( /TrackConfirmAction\Winput\.action\WtLabels\=(\d+)/g); var to = data.match(/Shipped.to.*[\r\n]*.*>([a-zA-Z\s-_]*)<br>/g); for(i in track_num){ var o = new Object(); var track = track_num[i].match(/(\d+)/g); var person = to[i].match(/>([a-zA-Z\s-_]+)<br>/); var myPerson = person[1].replace(/(\r\n|\n|\r)/gm,"") o["number"]=track[0]; o["carrier"]="USPS"; o["person"]=myPerson; out.push(o); } return out; }
You can parse all of your different shipping providers in one run of the script. After all of the shipment emails are read, it composes an email to PackageTrackr to give it all of the tracking numbers it just harvested.
var user = Session.getActiveUser().getEmail(); if(data.length > 0){ for(d in data){ body += this["formatForPackageTrackr"](data[d]["number"], data[d]["carrier"], data[d]["person"]); } GmailApp.sendEmail("track@packagetrackr.com", "Add Packages", body, {bcc: user}); } function formatForPackageTrackr(tracking_num, service, person){ return "#:" + tracking_num + " " + service + " " + person + "\n"; }
Down the line, other shipping providers could be added such as UPS and Fedex. Additionally, more tracking services could be added instead of just PackageTrackr.
Search engines have been using structured data for years to understand the information on web pages and provide richer search results. Today, we are introducing schemas in emails to make messages more interactive and allow developers to deliver a slice of their apps to users’ inboxes.
Schemas in emails can be used to represent various types of entities and actions. Email clients that understand schemas, such as Gmail, can render entities and actions defined in the messages with a consistent user interface. In the case of Gmail, this means that the emails can display quick action buttons that let users take actions directly from their inboxes, as in the following screenshot:
Using schemas to add quick action buttons to the emails you send is easy. All it takes is adding some markup to your HTML emails, together with your regular content, in one of the supported formats - Microdata and JSON-LD.
As an example, the following JSON-LD markup can be used to define a movie and the corresponding one-click action to add the movie to your queue:
<script type="application/ld+json"> { "@context": "schema.org", "@type": "Movie", "name": "The Internship", ... information about the movie ... "action": { "@type": "ConfirmAction", "name": "Add to queue", "actionHandler": { "@type": "HttpActionHandler", "url": "https://my-movies.com/add?movieId=123", "method": "POST", } } } </script>
Gmail renders the markup above with a button labelled “Add to queue” next to the email subject line. When the user clicks on the button, Gmail sends a POST request to the url specified in the action handler. Your app has to handle these requests and respond to the email client with an appropriate HTTP response code (200 for successful requests, 400 for invalid requests, etc.).
Schemas in emails currently support four different types of actions - rate/review, RSVP, one-click action and goto link - and we plan to add more types moving forward. We are collaborating with a number of partners who will launch their integrations in the coming weeks, making the messages they send more useful and interactive for Gmail users. For example, Esna is using this to inform users of missed calls and provide them with a one-click button to be called again, while Seamless is implementing the rate/review action to collect feedback about restaurants.
Other partners who are already implementing schemas in email today include both Billguard, Concur Technologies, Docusign, HelloSign, Insight.ly, Mailchimp, myERP, Netflix, OpenTable, Orangescape, Paperless Post, Spotify, SugarCRM, and Tripit.
To learn more about all supported entities and actions and to find out how to get started with schemas in email, visit http://developers.google.com/gmail.
As you can see, we’ve been working hard to improve Apps Script for you. We hope you enjoy the new features!