ACL (Access Control List) entries control who can access Google Docs resources. This allows more specific control over resource privacy or permissions.
Many types of applications need to grant document access for several users at once. As an example: when a new user is added to a project in the Manymoon project management application, every user on the project needs to be granted access to all attached Google docs. If there are 10 users on the project and 10 shared documents, this means the app would typically need to perform 100 HTTP requests -- a lot of overhead. With batching of ACL requests, the application can reduce the number of requests to one per document, resulting in a 10x savings.
A typical ACL entry for a single user is created by making an HTTP POST to the ACL link provided with each resource entry. The POST body looks something like this:
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gAcl='http://schemas.google.com/acl/2007'> <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/acl/2007#accessRule'/> <gAcl:role value='writer'/> <gAcl:scope type='user' value='new_writer@example.com'/> </entry>
To achieve the same thing using the Python client library, use the following code:
from gdata.acl.data import AclScope, AclRole from gdata.docs.data import AclEntry acl = AclEntry( scope = AclScope(value='user@example.com', type='user'), role = AclRole(value='writer') )
Instead of submitting the requests separately, multiple ACL operations for a resource can be combined into a single batch request. This is done by POSTing a feed of ACL entries. Each ACL entry in the feed must have a special batch:operation element, describing the type of operation to perform on the ACL entry. Valid operations are query, insert, update, and delete.
batch
batch:operation
query
insert
update
delete
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:gAcl='http://schemas.google.com/acl/2007' xmlns:batch='http://schemas.google.com/gdata/batch'> <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/acl/2007#accessRule'/> <entry> <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/acl/2007#accessRule'/> <gAcl:role value='reader'/> <gAcl:scope type='domain' value='example.com'/> <batch:operation type='insert'/> </entry> <entry> <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/acl/2007#accessRule'/> <id>https://docs.google.com/feeds/default/private/full/document%3Adocument_id/acl/user%3Aold_writer%40example.com</id> <gAcl:role value='writer'/> <gAcl:scope type='user' value='new_writer@example.com'/> <batch:operation type='update'/> </entry> </feed>
The following code represents the same operation in the Python client library:
from gdata.data import BatchOperation from gdata.acl.data import AclScope, AclRole from gdata.docs.data import AclEntry acl1 = AclEntry( scope=AclScope(value='example.com', type='domain'), role=AclRole(value='reader'), batch_operation=BatchOperation(type='insert') ) acl2 = client.get_acl_entry_by_self_link( ('https://docs.google.com/feeds/default/private/full/' 'document%3Adocument_id/acl/user%3Aold_writer%40example.com')) acl2.scope = AclScope(value='new_writer@example.com', type='user') acl2.role = AclRole(value='writer') acl2.batch_operation = BatchOperation(type='update') entries = [acl1, acl2]
The feed of these entries can now be submitted together to apply to a resource:
results = client.batch_process_acl_entries(resource, entries)
The return value is an AclFeed, with a list of AclEntry elements for each operation, the status of which can be checked individually:
for result in results.entry: print entry.title.text, entry.batch_status.code
The examples shown here are using the raw protocol or the Python client library. The Java client library also supports batch operations on ACL entries.
For more information on how to use batch operations when managing ACLs, see the Google Documents List API documentation, and the Google Data APIs batch protocol reference guide. You can also find assistance in the Google Documents List API forum.
Like any Google service, we’re always working to refine the Google Apps Marketplace for our vendors and their customers. Normally we make small, incremental improvements and let the better experience speak for itself, but this week we think several of the new features are noteworthy enough to point out.
A frequently requested feature has been improved analytics. If you have Google Analytics configured for your listing, your analytics profile will now receive the search terms and category selected by your customers.
When a customer searches on the Marketplace, the search results will all have two parameters attached: query and category. You’ll want to add these terms to your Google Analytics Website Profile. The query term will include all of the search terms the user entered into the search box, or it could be blank if the customer found your application through browsing. Similarly, the category parameter will be blank unless the customer narrowed his search or browse by picking the category you chose in your application listing, e.g. Accounting & Finance. Now you’ll have much better data about how a customer has reached your application, whether through browsing, searching, or a combination.
category
Another developer-focused enhancement we’ve made is adding a count of installs and uninstalls on each application’s listing when signed into the vendor account:
"Net install count" represents the number of current installs -- any uninstalls have already been deducted. If you add "Net install count" to "Uninstall count" you will have the total number of installations for the app since it launched. You are also able to retrieve this information from the Licensing API, but it is nice to have it easily accessible on your listing pages.
On the Vendor Profile page you’ll also that we’ve added the ability to hide unused applications (and show them again) so that you can manage your applications better and remove clutter on your dashboard. You still cannot delete applications, but you can sweep them under the rug! Note that Hiding and Unhiding are just for managing your list as a Vendor -- they do not change your Publish/Unpublish settings for each app. You can hide a published app as easily as an unpublished app.
Since announcing the Staff Picks program in May, we’ve featured a number of especially well-integrated and innovative applications on the @GoogleAtWork twitter stream and here on the Google Apps Developer Blog. Now we also feature Staff Picks on the front page of the Marketplace.
You can find four Staff Picks on the Marketplace landing page, chosen from the pool of apps selected as staff picks. See the Staff Picks page on code.google.com for more information on how we choose these apps.
Editors note: This is a guest post by Alex Steshenko. Alex is a core software engineer for Solve360, a highly-rated CRM in the Google Apps Marketplace which was recently chosen as a Staff Pick. Solve360 offers many points of useful integration with Google Apps. Today, in contrast to the conventional Data API integrations, Alex will showcase how he extended Solve360 using Google Apps Script. --- Ryan Boyd
Solve360 CRM integrates with Google services to provide a two-way contact & calendar sync, email sync and a comprehensive Gmail contextual gadget. We use the standard Google Data APIs. However, some of our use cases required us to use Google documents and spreadsheets. Enter Apps Script!. What brought our attention to Google Apps Script was that it allows you to run your application code right within the Google Apps platform itself, where documents can be manipulated using a wide range of native Google Apps Script functions, changing the perspective.
Our first experience with Google Apps Script was writing a "Contact Us" form. We decided to use the power and flexibility of Apps Script again to generate different kinds of reports.
Google Spreadsheets can produce rich reports leveraging features such as filters, pivot tables, built-in functions and charts. But where’s the data to report on? Using Google Apps Script, users can integrate Google Spreadsheets with a valuable source of data - the Solve360 CRM - completing the solution.
Solve360 Google Apps Reporting script lets users configure the reporting criteria while pulling reports into a Google Spreadsheet.
Here's a video demonstrating a real use case for Solve360 Reporting:
For this script, we realized, simply providing spreadsheet functions would not be good enough. We needed a user interface to let users configure their account details and define what kind of data to fetch from the Solve360 CRM. Google Apps Script’s Ui Services came in handy. For instance, here is the function responsible for showing the “Solve360 account info” dialog:
/* * Creates new UI application and opens setting window */ function settingsUi() { var app = UiApp.createApplication(); app.setTitle('Solve360 account info') .setWidth(260) .setHeight(205); var absolutePanel = app.createAbsolutePanel(); absolutePanel.add(authenticationPanel_(app)); app.add(absolutePanel); SpreadsheetApp.getActiveSpreadsheet().show(app); }
Solve360 CRM has an external API available so the system can be integrated with custom business applications and processes. Reporting script use case is a good example of what it can be used for.
One of the first tricks learned was creating our own Google Apps-like “service” to encapsulate all those functions responsible for interacting with Solve360 CRM’s API. What is the most interesting is that this service’s code isn’t a part of the distributed Google Apps script. Instead the library is loaded from within the script itself directly from our servers. Why? Let’s say we found a bug or added new functions - if we had many copies of the service we would need to update them all, somehow notifying our clients, and so on. With one source, there’s no such problem. You may think of it as a way to distribute a Google Apps Script solution, or, in our case, a part of the solution. The service is called Solve360Service and its usage looks like this:
Solve360Service
var contact = Solve360Service.addContact(contactData);
There were two problems with getting such an external service to work: Google Apps Script permissions restrictions and actually including it in the script.
The issue with permissions is that the Google Apps Script environment can’t see which Google Apps Script services are used inside the external service - that’s why it doesn’t ask you to grant special permissions for them. To force the authorization request for those permissions we added this to the onInstall function (called once when script is added to the spreadsheet):
onInstall
function onInstall() { // to get parseJS permissions Xml.parseJS(['solve360', '1']); // to get base64Encode permissions Utilities.base64Encode('solve360'); // ... }
Here is the solution we used to load our centralized code into the script:
eval(UrlFetchApp.fetch("https://secure.solve360.com/gadgets/resources/js/Solve360Service.js").getContentText());
The Solve360Service is loaded from a single source - no copy-paste. All the functions for accessing the Solve360 API aka “the plumbing” are abstracted and hidden in inside this service, while the essentials of the reports script itself can be modified and tweaked to a particular client’s case. Inside of Solve360Service we use UrlFetchApp:
UrlFetchApp
/** * Request to the Solve360 API server * data should be an Array in Short Hand notation */ request : function(uri, restVerb, data) { if (this._credentials == null) { throw new Error('Solve360 credentials are not set'); } if (typeof(data) != 'undefined') { if (restVerb.toLowerCase() == 'get') { var parameters = []; for each(var parameter in data) { parameters.push(encodeURIComponent(parameter[0]) + '=' + encodeURIComponent(parameter[1])); } uri += '?' + parameters.join('&'); data = ''; } else { data.unshift('request'); data = Xml.parseJS(data).toXmlString(); } } else { data = ''; } var options = { "contentType" : "application/xml", "method" : restVerb.toLowerCase(), "payload" : data, "headers" : {"Authorization" : "Basic " + this._credentials} }; return Xml.parse(UrlFetchApp.fetch(this._url + uri, options).getContentText()).getElement(); }
As the result is always XML, in order to remove any extra work we call Xml.parse() right inside the request function and always return a XmlElement so you can iterate through it, access nodes and attributes. Here is a simplified version of how we load some items when building a report:
Xml.parse()
XmlElement
/* * Builds a search config from user preferences and loads a slice of data from Solve360 * To configure how many items should be loaded at a time, change ITEMS_LOAD_REQUEST_LIMIT constant */ function retrieveItems_(parameter, offset) { initSolve360Service_(); // ... var searchParameters = [ ['layout', '1'], ['sortdir', 'ASC'], ['sortfield', 'name'], ['start', '' + offset], ['limit', '' + ITEMS_LOAD_REQUEST_LIMIT], ['filtermode', filtermode], ['filtervalue', filtervalue], ['searchmode', searchmode], ['searchvalue', searchvalue], ['special', special], ['categories', '1'] ]; if (parameter.showAllFieldsCheckbox != 'true' && fields.length > 0) { searchParameters.push(['fieldslist', fields.join(',')]); } // ... var items = Solve360Service.searchProjectBlogs(searchParameters); // ... return items; }
To simplify the usage of the service we added another function which initializes the service object, named Solve360Service:
/* * Loads external Solve360Service * For the service functions available refer to the source code here: * https://secure.solve360.com/gadgets/resources/js/Solve360Service.js */ var Solve360Service = null; function initSolve360Service_() { if (Solve360Service == null) { eval(UrlFetchApp.fetch("https://secure.solve360.com/gadgets/resources/js/Solve360Service.js").getContentText()); var user = UserProperties.getProperty(USERPROPERTY_USER); var token = UserProperties.getProperty(USERPROPERTY_TOKEN); if (user == null || user.length == 0 || token == null || token.length == 0) { throw new Error('Use Solve360 spreadsheet menu to set email and token first'); } Solve360Service.setCredentials(user, token); } }
As you can see, it uses the email/token pair previously saved in the “Solve360 Account Info” dialog or signals an error if the credentials were not yet saved.
There are many use cases where you can apply the Google Apps Script. The fact that you can work and implement solutions right from “inside” one of the greatest and most universal web applications available is amazing.
You can integrate your own software with Google Docs or even learn from us and build a reporting script for any other system accessible online. Try to look at solving business tasks from a different perspective, from the Google Apps point of view. We encourage it!
The code of the new script is available for use and study here: https://secure.solve360.com/docs/google-apps-reports.js.
Alex Steshenko is a core software engineer for Solve360, a CRM application on the Google Apps Marketplace.
We have released version 1.9 of the .NET Library for Google Data APIs and it is available for download.
This version adds the following new features:
This new version also removes the client library for the deprecated Google Base API and fixes 20 bugs.
For more details, please check the Release Notes and remember to file feature requests or bugs in the project issue tracker.
function doGet() { // Populate the DataTable. We'll have the data labels in // the first column, "Quarter", and then add two data columns, // for "Income" and "Expenses" var dataTable = Charts.newDataTable() .addColumn(Charts.ColumnType.STRING, "Quarter") .addColumn(Charts.ColumnType.NUMBER, "Income") .addColumn(Charts.ColumnType.NUMBER, "Expenses") .addRow(["Q1", 50, 60]) .addRow(["Q2", 60, 55]) .addRow(["Q3", 70, 60]) .addRow(["Q4", 100, 50]) .build();
SpreadsheetApp
UiApp
UrlFetch
// Build the chart. We'll make income green and expenses red // for good presentation. var chart = Charts.newColumnChart() .setDataTable(dataTable) .setColors(["green", "red"]) .setDimensions(600, 400) .setXAxisTitle("Quarters") .setYAxisTitle("$") .setTitle("Income and Expenses per Quarter") .build();
setDataTable()
build()
// Add our chart to the UI and return it so that we can publish // this UI as a service and access it via a URL. var ui = UiApp.createApplication(); ui.add(chart); return ui;}
// Save the chart to our Document List var file = DocsList.createFile(chart); file.rename("Income Chart"); file.addToFolder(DocsList.getFolder("Charts")); // Attach the chart to the active sites page. var page = SitesApp.getActivePage(); page.addHostedAttachment(chart, "Income Chart"); // Attach the chart to an email. MailApp.sendEmail( "recipient@example.com", "Income Chart", // Subject "Here's the latest income chart", // Content {attachments: chart });
In March, we announced that we would start requiring clients to use SSL when making requests to the Google Documents List API, the Google Spreadsheets API, and the Google Sites API. This is part of our ongoing effort to increase the security of user data.
The time has come, and we are starting to roll out this requirement. On average, about 86% of requests to these APIs are already using SSL, so we expect there to be minimal migration required. The implementation will continue throughout September. If an application receives an HTTP 400 Bad Request response to a request, then it may be because the request was not made using HTTPS.
Clients that have not already started using SSL for all requests should do so immediately. This is as simple as upgrading to the latest version of the relevant API client library. Developers with questions should post in the API forums.
gContact:status
<gcontact:status indexed="true"/>
indexed
false
AccountManager
GoogleAccountManager
GoogleAccountManager googleAccountManager = new GoogleAccountManager( activity); Account[] accounts = accountManager.getAccounts();
AccountManager.getAuthToken()
AccountManagerCallback
googleAccountManager.manager.getAuthToken(account, AUTH_TOKEN_TYPE, null, activity, new AccountManagerCallback<Bundle>() { public void run(AccountManagerFuture<Bundle> future) { try { // If the user has authorized your application to use the tasks API // a token is available. String token = future.getResult().getString( AccountManager.KEY_AUTHTOKEN); // Now you can use the Tasks API... useTasksAPI(token); } catch (OperationCanceledException e) { // TODO: The user has denied you access to the API, you // should handle that } catch (Exception e) { handleException(e); } } }, null);
AUTH_TOKEN_TYPE
String AUTH_TOKEN_TYPE = ”Manage your tasks”;
useTasksAPI(String accessToken) { // Setting up the Tasks API Service HttpTransport transport = AndroidHttp.newCompatibleTransport(); AccessProtectedResource accessProtectedResource = new GoogleAccessProtectedResource(accessToken); Tasks service = new Tasks(transport, accessProtectedResource, new JacksonFactory()); service.setKey(INSERT_YOUR_API_KEY); service.setApplicationName("Google-TasksSample/1.0"); // TODO: now use the service to query the Tasks API }
service
import gdata.apps.emailsettings.client import gdata.contacts.client # replace these values with yours CONSUMER_KEY = 'mydomain.com' CONSUMER_SECRET = 'my_consumer_secret' company_name = 'ACME Inc.' admin_username = 'admin'
xoauth_requestor_id
# request a 2-legged OAuth token requestor_id = admin_username + '@' + CONSUMER_KEY two_legged_oauth_token = gdata.gauth.TwoLeggedOAuthHmacToken( CONSUMER_KEY, CONSUMER_SECRET, requestor_id) # Email Settings API client email_settings_client = gdata.apps.emailsettings.client.EmailSettingsClient( domain=CONSUMER_KEY) email_settings_client.auth_token = two_legged_oauth_token # User Profiles API client profiles_client = gdata.contacts.client.ContactsClient( domain=CONSUMER_KEY) profiles_client.auth_token = two_legged_oauth_token
HtmlSignature()
# helper class used to build signatures class SignatureBuilder(object): def HtmlSignature(self): signature = '%s' % self.name if self.occupation: signature += '%s' % self.occupation if self.company: signature += '%s' % self.company signature += 'Email: <a href=\'mailto:%s\'>%s</a> - Phone: %s' % ( self.email, self.email, self.phone_number) return signature def __init__( self, name, company='', occupation='', email='', phone_number=''): self.name = name self.company = company self.occupation = occupation self.email = email self.phone_number = phone_number
GetProfilesFeed()
next
# get all user profiles for the domain profiles = [] feed_uri = profiles_client.GetFeedUri('profiles') while feed_uri: feed = profiles_client.GetProfilesFeed(uri=feed_uri) profiles.extend(feed.entry) feed_uri = feed.FindNextLink()
SignatureBuilder
name
company
occupation
email
phone_number
# extract relevant pieces of data for each profile for entry in profiles: builder = SignatureBuilder(entry.name.full_name.text) builder.company = company_name if entry.occupation: builder.occupation = entry.occupation.text for email in entry.email: if email.primary and email.primary == 'true': builder.email = email.address for number in entry.phone_number: if number.primary and number.primary == 'true': builder.phone_number = number.text # build the signature signature = builder.HtmlSignature()
UpdateSignature
# entry.id has the following structure: # http://www.google.com/m8/feeds/profiles/domain/DOMAIN_NAME/full/USERNAME # the username is the string that follows the last / username = entry.id.text[entry.id.text.rfind('/')+1:]
# set the user's signature using the Email Settings API email_settings_client.UpdateSignature(username=username, signature=signature)
var MARK_UNREAD = false; var ADD_UNSNOOZED_LABEL = false;
setup()
function getLabelName(i) { return "Snooze/Snooze " + i + " days"; } function setup() { // Create the labels we’ll need for snoozing GmailApp.createLabel("Snooze"); for (var i = 1; i <= 7; ++i) { GmailApp.createLabel(getLabelName(i)); } if (ADD_UNSNOOZED_LABEL) { GmailApp.createLabel("Unsnoozed"); } }
function moveSnoozes() { var oldLabel, newLabel, page; for (var i = 1; i <= 7; ++i) { newLabel = oldLabel; oldLabel = GmailApp.getUserLabelByName(getLabelName(i)); page = null; // Get threads in "pages" of 100 at a time while(!page || page.length == 100) { page = oldLabel.getThreads(0, 100); if (page.length > 0) { if (newLabel) { // Move the threads into "today’s" label newLabel.addToThreads(page); } else { // Unless it’s time to unsnooze it GmailApp.moveThreadsToInbox(page); if (MARK_UNREAD) { GmailApp.markThreadsUnread(page); } if (ADD_UNSNOOZED_LABEL) { GmailApp.getUserLabelByName("Unsnoozed") .addToThreads(page); } } // Move the threads out of "yesterday’s" label oldLabel.removeFromThreads(page); } } } }
app:edited
https://docs.google.com/feeds/default/private/changes
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:docs="http://schemas.google.com/docs/2007" xmlns:gd="http://schemas.google.com/g/2005" gd:etag="W/"DEEMQ3w8eyt7ImA9WhZUGUo.""> <docs:largestChangestamp>5635</docs:largestChangestamp> <link rel="next" type="application/atom+xml" href="...?start-index=5636"/> <entry gd:etag="W/"DUcMRHg5cCt7ImA9WhZUGUo.""> <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#change" label="change"/> <title>Project tasks</title> ... <docs:changestamp value="5623"/> </entry> ...</feed>
https://www.googleapis.com/tasks/v1/users/username/lists?key=<API_KEY>
Tasks
// Initializing the Tasks API serviceTasks service = new Tasks("2-LO Tasks Test", httpTransport, jsonFactory);service.accessKey = API_KEY;
Posted by John Gale, Solution Developer, Appogee
Want to weigh in on this topic? Discuss on Buzz
function doGet() { var app = UiApp.createApplication(); var component = app.loadComponent(“MyGui”); app.add(component); return app;}
loadComponent
app.getElementById()
var button = app.getElementById(“Button1”);button.setStyleAttribute(“color”, “red”);
function doGet() { var app = UiApp.createApplication(); var component = app.loadComponent(“MyGui”); app.add(component.getElementById(“Pane1l”)); return app;}
app.add(app.loadComponent(“MyGui”, {prefix: “a”})); app.add(app.loadComponent(“MyGui”, {prefix: “b”}));
sheet.show()