Using Google Apps Scripts as a back end

I recently built a quick internal web app for mobiles using Sencha Touch, and I needed a back end system for very simple content management. As this was a lightweight project without a need for ongoing support and with no expectation of high use, I didn’t want to build a whole back end, or pay for an out of the box solution that would feel like overkill – and not having control of the server was a factor too.

I decided to use Google Spreadsheets for the ad hoc back end, knowing they had an API that supports JSON, but not having explored it before. Sencha Touch comes with handy AJAX and JSONP handlers that I wanted to use as well; the JSON needs to be clean and in a useful format for this to work out of the box – unfortunately what the Spreadsheets API gives you is very complete and generic, and doesn’t for example associate column headers with every JSON object it returns; you need to parse the data yourself to make the format clean enough for Sencha to consume.

Here’s a sample of the sort of data I wanted to administer

Here’s the Spreadsheet API’s default output for that sheet…

https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic2013-05-16T11:11:51.102ZSheet1 meme@marcelkornblum.com91https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R1C12013-05-16T11:11:51.102ZA1name https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R1C22013-05-16T11:11:51.102ZB1email address https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R1C32013-05-16T11:11:51.102ZC1customerid https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R2C12013-05-16T11:11:51.102ZA2Marcel Kornblum https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R2C22013-05-16T11:11:51.102ZB2me@mydomain.com https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R2C32013-05-16T11:11:51.102ZC20000001 https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R3C12013-05-16T11:11:51.102ZA3Someone Else https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R3C22013-05-16T11:11:51.102ZB3someone@else.com https://spreadsheets.google.com/feeds/cells/0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E/1/private/basic/R3C32013-05-16T11:11:51.102ZC30000002

…which you can see is pretty complex and raw. Here’s the JSONP response I was hoping for:

callbackFunction(
{
data: [
{
name: "Marcel Kornblum",
emailAddress: "me@mydomain.com",
customerid: "0000001"
},
{
name: "Someone Else",
emailAddress: "someone@else.com",
customerid: "0000002"
}
]
}
)

After a bit of digging around I found Google Apps Scripts: I’d heard of these before but never figured out how to use them or what they were for. In a nutshell, it’s a very powerful and flexible system that plugs into your Google Docs and that can be deployed in a huge number of ways, from allowing you to create custom functions that are exposed within specific spreadsheets (so users can put their data through your specific algorithm), all the way to creating public facing web forms and sites. One of the things it allows is JSON and JSONP responses to public facing URLs, supporting GET variables and having easy Docs API handlers.

The documentation is extensive, but it took me a long time to figure out whether or not this would be possible, which is why I’m writing this post – basically a how-to for getting spreadsheet and calendar data into a nicely formatted and sensible JSONP structure that’s web-accessible, and that can then be used to power your prototypes and small web apps.

So let’s get started.

Obviously you need a spreadsheet to start with – here’s the one I’m using for this tutorial. First of all, you should publish it to the web; this will allow you to access the data from the Scripts API without having to implement authentication (although you can turn that on if you’d prefer).

Google Spreadsheet Publish to Web menu option

Next, you need to define a Data Range. This is basically a named way of accessing a set of cells you define as important, and we’ll use it in the Script. The easiest way to set one up is to highlight all the content cells (don’t include the headers) and add as many extra rows as you think you might need. Then use the menu to get to Data > Named and Protected Ranges, and you’ll see a sidebar appear, as in the screenshots below.

Google Spreadsheet Data Range sidebar Google Spreadsheet Data Range menu option

Once you’ve got that set up, put some dummy data in so we can see when it’s working, and it’ll be time to start scripting.

The first time you make a Google Apps Script you need to set it up in your Drive account, as follows:

Go to Google Drive and select Create > Connect more apps as shown below. You’ll see an overlay with a huge number of options for apps you can connect to your account – you can search for ‘script’ and it should be your top result.

Google Drive Create window            Google Drive Apps overlay

Once you’ve connected the Script app, the next time you hit “create” you’ll get an option to make a script. Go ahead and make one – I followed this tutorial, which goes through connecting to the spreadsheet, getting the date out of the ranges, associating it with header cells and formatting the JSON objects.

In order to make your script accessible on a URL you need to save a version of it, and then deploy it as a web app; although you can access the unversioned script for testing through the tiny link on the web app confirmation overlay as seen here.

Google App Script Deploy Web App

To make your script return JSONP you need to wrap the JSON inside a function call; the function name needs to be variable in order to work with Sencha’s system out of the box.

Luckily Google Apps Scripts can accept GET variables which allow you to easily capture the function name you want and then simply string concatenate the output like so:

function doGet(e) {
var cb = e.parameter.cb;
var outputStr = cb + '(' + JSON.stringify(output) + ');';
return ContentService.createTextOutput(outputStr).setMimeType(ContentService.MimeType.JSON);
}

The only other things I ended up changing were allowing the spreadsheet ID, worksheet name and data range to all be passed in as variables (because I wanted to use different pieces of managed data in my app without duplicating the script), and a tweak letting empty cells show up as object properties with empty strings instead of being skipped.

Here’s my final code (and here as a script) – it’s almost identical to the guide code I followed as you’ll see :)


function doGet(e) {
var cb = e.parameter.cb;
var sheetno = e.parameter.sn;
var sheetid = e.parameter.s;
var datarange = e.parameter.dr;
var ss = SpreadsheetApp.openById(sheetid);
var sheet = ss.getSheetByName(sheetno);
// Get the range of cells that store employee data.
var eDataRange = ss.getRangeByName(datarange);
// For every row of employee data, generate an employee object.
var resultObjects = getRowsData(sheet, eDataRange);
var output = {'data':resultObjects}
var outputStr = cb + '(' + JSON.stringify(output) + ');'
return ContentService.createTextOutput(outputStr).setMimeType(ContentService.MimeType.JSON);
// get DB obj
//var db = ScriptDb.getMyDb();
//var saveResults = db.saveBatch(resultObjects, false);
//Logger.log(resultObjects);
}
// getRowsData iterates row by row in the input range and returns an array of objects.
// Each object contains all the data for a given row, indexed by its normalized column name.
// Arguments:
// - sheet: the sheet object that contains the data to be processed
// - range: the exact range of cells where the data is stored
// - columnHeadersRowIndex: specifies the row number where the column names are stored.
// This argument is optional and it defaults to the row immediately above range;
// Returns an Array of objects.
function getRowsData(sheet, range, columnHeadersRowIndex) {
columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;
var numColumns = range.getLastColumn() - range.getColumn() + 1;
var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);
var headers = headersRange.getValues()[0];
return getObjects(range.getValues(), normalizeHeaders(headers));
}
// getColumnsData iterates column by column in the input range and returns an array of objects.
// Each object contains all the data for a given column, indexed by its normalized row name.
// Arguments:
// - sheet: the sheet object that contains the data to be processed
// - range: the exact range of cells where the data is stored
// - rowHeadersColumnIndex: specifies the column number where the row names are stored.
// This argument is optional and it defaults to the column immediately left of the range;
// Returns an Array of objects.
function getColumnsData(sheet, range, rowHeadersColumnIndex) {
rowHeadersColumnIndex = rowHeadersColumnIndex || range.getColumnIndex() - 1;
var headersTmp = sheet.getRange(range.getRow(), rowHeadersColumnIndex, range.getNumRows(), 1).getValues();
var headers = normalizeHeaders(arrayTranspose(headersTmp)[0]);
return getObjects(arrayTranspose(range.getValues()), headers);
}
// For every row of data in data, generates an object that contains the data. Names of
// object fields are defined in keys.
// Arguments:
// - data: JavaScript 2d array
// - keys: Array of Strings that define the property names for the objects to create
function getObjects(data, keys) {
var objects = [];
for (var i = 0; i < data.length; ++i) {
var object = {};
var hasData = false;
for (var j = 0; j < data[i].length; ++j) {
var cellData = data[i][j];
/*
EDITED TO HAVE EMPTY STRINGS FOR EMPTY CELLS
if (isCellEmpty(cellData)) {
continue;
}
*/
if (isCellEmpty(cellData)) {
cellData = '';
}
else
{
hasData = true;
}
object[keys[j]] = cellData;
}
if (hasData) {
objects.push(object);
}
}
return objects;
}
// Returns an Array of normalized Strings.
// Arguments:
// - headers: Array of Strings to normalize
function normalizeHeaders(headers) {
var keys = [];
for (var i = 0; i < headers.length; ++i) { var key = normalizeHeader(headers[i]); if (key.length > 0) {
keys.push(key);
}
}
return keys;
}
// Normalizes a string, by removing all alphanumeric characters and using mixed case
// to separate words. The output will always start with a lower case letter.
// This function is designed to produce JavaScript object property names.
// Arguments:
// - header: string to normalize
// Examples:
// "First Name" -> "firstName"
// "Market Cap (millions) -> "marketCapMillions
// "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored"
function normalizeHeader(header) {
var key = "";
var upperCase = false;
for (var i = 0; i < header.length; ++i) { var letter = header[i]; if (letter == " " && key.length > 0) {
upperCase = true;
continue;
}
if (!isAlnum(letter)) {
continue;
}
if (key.length == 0 && isDigit(letter)) {
continue; // first character must be a letter
}
if (upperCase) {
upperCase = false;
key += letter.toUpperCase();
} else {
key += letter.toLowerCase();
}
}
return key;
}
// Returns true if the cell where cellData was read from is empty.
// Arguments:
// - cellData: string
function isCellEmpty(cellData) {
return typeof(cellData) == "string" && cellData == "";
}
// Returns true if the character char is alphabetical, false otherwise.
function isAlnum(char) {
return char >= 'A' && char <= 'Z' || char >= 'a' && char <= 'z' || isDigit(char); } // Returns true if the character char is a digit, false otherwise. function isDigit(char) { return char >= '0' && char <= '9';
}
// Given a JavaScript 2d Array, this function returns the transposed table.
// Arguments:
// - data: JavaScript 2d Array
// Returns a JavaScript 2d Array
// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].
function arrayTranspose(data) {
if (data.length == 0 || data[0].length == 0) {
return null;
}
var ret = [];
for (var i = 0; i < data[0].length; ++i) {
ret.push([]);
}
for (var i = 0; i < data.length; ++i) {
for (var j = 0; j < data[i].length; ++j) {
ret[j][i] = data[i][j];
}
}
return ret;
}

Here you can see the JSONP output of the script acting on the spreadsheet – and doesn’t it look lovely!

https://script.google.com/macros/s/AKfycby8snUVINBHncRzPumnjMGgbg0r23gpoDzpzJ4HzK85t_bDFo4/exec?s=0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E&sn=Sheet1&dr=testRange&cb=callbackFunction

Of course, this probably isn’t the best thing to use in a production environment as there might well be usage restrictions and it’s definitely not built to be bulletproof, but for knocking together  a quick prototype and letting staff edit content in an environment they’re used to, it seems like a decent enough solution to me.

If you’re interested, this is how I called it from the Sencha Store JSONP Proxy:

Ext.define('MyApp.store.Demo', {
extend: 'Ext.data.Store',
config: {
storeId: "demoStore",
model: "MyApp.model.Demo",
autoLoad: true,
proxy: {
type: 'jsonp',
callbackKey: 'cb',
url: 'https://script.google.com/macros/s/AKfycby8snUVINBHncRzPumnjMGgbg0r23gpoDzpzJ4HzK85t_bDFo4/exec?s=0AsVzV2H8zZOedHc4bDUwX0QzYnhXdDFFWE1jcEpXN0E&sn=Sheet1&dr=testRange',
reader: {
type: 'json',
rootProperty: 'data'
},
},
}
});

I hope this is useful to you!

Does enjoying your job make it more expensive?

Or does it being expensive mean you enjoy it more?

[This post was originally published accidentally in draft form; I've written it up now - sorry if it was confusing before!]

The beginning of the train of thought that got me here was an impression that quality work is created by passionate people. I’m currently in Amsterdam at the Craften festival, where the theme really is about putting passion into your work and getting really polished results. A lot of digital work is undertaken in sweatshop-style environments, usually (but not entirely) offshore.

People often hire offshore developers to do well understood, well defined tasks: creativity not required. This is because the perception is that the quality of creative there is lower – and of course because anyone can do the boring stuff, so why not send it somewhere cheap, right?

As a market develops there is more call for creative in all aspects of production – be it woodwork or digital – as people become more sophisticated in their tastes. This gives makers the opportunity to stretch their creative wings, which I personally equate to your job being more enjoyable.

I feel sorry for creatively minded codes and designers in young markets, where the briefs they see tend to be very dry and boring. At the same time, the majority of offshore developers don’t seem to merit more creative briefs (with some notable exceptions) – there are simply so many people around the world willing to work for low salaries (as seen from the UK market) that the crowd in general gets a reputation for bad work.

All of which leads me to the question: as you start to enjoy your work, and presumably to do better, more effective work, does that lead to it being more expensive? Or does the marketplace developing to support more expensive services create the space for people to work in those roles, and for those people to enjoy their jobs more.

I suspect – like many things – that it’s a bit of both that have to develop in an iterative cycle…

Rediscovering Tasker for Android

If you use Android and, like me, you enjoy tinkering with settings and customising your phone, you should really have already heard about Tasker. It just received an update making the UI look more friendly, but it’s the same app it’s been for the last few years. At £1,99 it’s a real bargain and you should definitely get it.

I hadn’t used it for a while but after buying my Nexus 4 I decided I wanted a few personalised settings and downloaded it again; it’s been really amazing exploring the interface and rediscovering its power.

Basically the premise of Tasker is that you can script any action on your phone (from settings to App launching to triggering external events), based on any input your phone can detect (from which cell towers you’re near, what’s in your calendar or what time it is). This means that, once you’ve got your head around the (admittedly confusing) interface you can really customise any aspect of your phone. Once you factor in all the plugins available, you’ll be able to make all sorts of security settings changes as well as sending messages between devices (or your browser), make calls or texts, etc.

Personally, I’ve got tasks set up based broadly on Location, Time and Power. For example, when the battery goes below 25% and isn’t being charged, the phone switches off all data connections and adjusts screen brightness and timeout to conserve power. It also puts a message in the notification bar; tapping this overrides the setting so you can use network connections until you go back to the low power settings.

Another useful script I’ve got mutes the phone and leaves vibrate on, if there’s any current event in my Work calendar. At work and home I switch Wifi on and lower the volume, and during nighttime before a work day the phone goes to silent if I’m at home.

Another nice tweak is that every morning, the phone checks the weather report for the day and reads it out to me just after my alarm goes off.

There are absolutely tons of these scripts online, and making them yourself is pretty easy too, though you’ll want to get your head around variables in order to stop the complexity from causing conflicts.

In case you’re interested, here’s an export of all my tasks – you’ll have to customise settings to make it work for your times and locations but feel free to upload and use!

…and let me know which tasks you find most useful, so I can add some more :)

Space Cookery

I love this video. Commander Hadfield is very active on Twitter (twitter.com/cmdr_hadfield), sharing amazing photos of places he’s passing over while on board the ISS.

In this video he goes through the process of making a penaut butter and honey tortilla wrap in space; what’s amazing is you don’t really twig that he’s in zero gravity until he lets go of something and it just hangs there….which is amazing.

My Chrome Extensions

It’s been a while since I posted, and given that most of what I’ve been working on recently is either confidential or I’ve already been saying it a lot (you’ve checked out www.stinkxt.com by now right?), I thought I’d run you through the Chrome Extensions I’ve got installed and use all the time.

I’ve grouped them so you can skim past any that don’t interest you, in case this all seems a bit overkill :-)

 

Unobtrusive but essential

Google Quick Scroll – “Quick Scroll lets you jump directly to the relevant bits of a Google search result.”

Basically, this rocks. If you search for a term and get a wikipedia or forum page as a result, you often have to sift through tons of irrelevant content before you get to the bit that deals with your search term. This plugin puts a little box at the corner of your screen (only in this situation); clicking the box scrolls you down to the highlighted text that Google picked as your result. Neat!

HoverZoom – “Enlarge thumbnails on mouse over.”

Very simple, does what it says on the tin (to quote a hateful man). Works great on a lot of sites, saves a lot of clicks on image searches, and you can disable it for specific sites if it gets annoying. Perfect for Google’s new animated image search (the GIFs animate in the overlay, but not on Google’s result page).

FasterChrome – “Get quick definitions, auto-load next pages, search faster, and more.”

This is an extension that comes with a ton of features, almost all of which end up being really really annoying. But, it does have one killer feature, which is why it’s here: autoload next page. On most sites with pagination, as you scroll to the bottom, this little gem will load up the next page (disabling JS on it BTW) and render it directly underneath a little divider bar. Infinite scroll for everything! Just remember to turn off all the other guff it comes with.

 

Security

Lastpass - “LastPass is a free password manager and form filler.”

I can’t overstate how awesome this is. Log in to the extension, and it’ll auto log you in to any site you go to – as soon as it sees a sign in form it recognises it will just fill the details and hit send; you sometimes just see the page flicker for a second before being logged in. It has an absolute ton of other features, is really secure, and has got me out of a bind lots of times. Use this.

Ghostery – “See who’s tracking your web browsing with Ghostery.”

I was recently turned on to this one by a post on the EFF site about Facebook tying your online and offline data together, and how to minimise their ability to do so. It’s worth a read, but in any case you might want to install this as it not only block all sorts of trackers, like and tweet buttons and things, but it also shows you how many are on each page you visit (TechCrunch is pretty scary!). I decided to allow a couple of analytics tools as it means site owners can improve their sites based on my behaviour, and I “pause” blocking when I’m testing stuff :)

 

For Android users

MightyText - “Text from Chrome! Sync’d with your Android”

This beauty comes with a free Android app that you put on your phone, and that has no user functionality once you’ve signed in with your Google account. The extension puts a button by your Omnibar that you click through to a full webpage-rendered view of all your text messages that are on the phone, along with the ability to send them via Chrome! WOOP! The extension sends them to the phone, which then texts them, so careful of those costs. It even gives you a notification if your phone rings – handy if it’s on silent at work…

Chrome to Phone - “Chrome to Phone enables you to send links and other information from Chrome to your Android device.”

Basically, click the little button on Chrome and the page you’re on loads on your phone. Super useful, although this might be a bit defunct with Chrome Tab Sync, and also with AutoRemote for Tasker (more on that in my next post!)

 

Other

TabCloud - “Save and restore window sessions over time and across multiple computers.”

Do you open lots of tabs? Do you keep them open because they’re all relevant to a bit of research, or something that you’re not doing right now but intend to pick up again later? Get this extension.

 

Geeky

Vimium - “The Hacker’s Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.”

Nuff said.

AutoRemote - “Sends messages to your Android Device through AutoRemote”

More on this in my next post, about the awesomeness of Tasker.

 

I hope those are useful for you – let me know if there are any other must-haves I’m missing :)

My new project: Stink Xt

After almost two years of discussions, I’ve just started my new project, funded by Stink and Stinkdgital, the company(ies) I’ve been working for for the last 4.5 years.

I’m still working on a web page to outline what it’s for more concisely, but here’s a rundown copied from an all staff email sent out to Stink and Sitnkdigital staffers today:

Xt is here to help you make your ideas into products.

All of us probably have one or two things in our minds that we’ve thought about for a while, but never had the free time, the contacts or the skills to develop further. The plan is to actually start creating these and try to make money out of them. They could be apps, tools, websites or really anything to do with digital and/or media.

Over the next few weeks I’ll be talking to everyone who’s interested, and trying to figure out which ideas to start with. We’ll research how much money it would take to build your idea and how much we think could be made from it. We’ll figure out the best way to create the product and start talking to people who might get involved. We’ll put a report and presentation of our findings together, and the ideas with the most potential will be shown to investors in about 3 months time.

You will retain ownership of your idea with no commitment to proceed with us; if everyone is excited and wants to put money into building your idea then we’ll sit down together and figure out a fair split of ownership based on what everyone puts in.

It’s open to everyone, so if you have an idea you’d like to talk about, please get in touch (my work email is now marcel@stinkxt.com) and we’ll set up a time to chat. You don’t need to prepare an elaborate presentation, and you don’t have to have done a lot of work or research into it – that’s what I’m here for (though if you have done a lot of work on it that’s great too).

Let me know what you think in the comments, or feel free to get in touch directly to talk about how it works, if you’re interested in helping out, or if you have an idea you want to talk about!