Automate personalising and sharing documents with Apps Script

As a manager I spend a fair amount of time in… Google Docs!

While I don’t mind writing things which are not code, I do dislike tedious tasks… specially tasks that can be automated. One of these is creating separate documents per person using a template. You have to open the template, make a copy, change things, share it with the report… TEDIOUS! 😤🙅🏻‍♀️

Fortunately, there’s a solution to automate this: Apps Script!

It exposes the same apps you’re used to interact with, but programmatically. And so I translated the sequence from above into code. I can now do in 22 seconds what used to take me 15 minutes and a considerable amount of frustration building. And because I’m VERY NICE I’m sharing the code with you, so you can also save time and do better things with your brain. You’re very welcome 💓

Apps Script is basically JavaScript, but it runs in The Cloud. To access the dashboard you need to be logged in with a Google account and then go to https://script.google.com/. Then create a new project and paste the following in the editor:

var docs = [
  // The id is the string you see in the document url:
  // https://docs.google.com/document/d/{the very long string here}/edit
  { id: 'documentID1', title: 'PERSON:Your name 1:1 - YYYY' },
  { id: 'documentID2', title: 'PERSON YYYY goals' }
];

var people = [
  { name: 'Person number one', email: 'personnumberone@example.com' },
  // More people if you need
  // I recommend you test with your own address before sending to everyone!
];

function main() {
  people.forEach(function(person) {
    docs.forEach(function(item) {
      var newTitle = personalise(item.title, person);
      var fileClone = clone(item.id);
      var clonedId = fileClone.getId();
      var docClone = DocumentApp.openById(clonedId);
     
      fileClone.setName(newTitle);

      // Replace template tags
      personaliseDoc(docClone, person);

      // Share with person's email, read+write
      fileClone.addEditor(person.email);
    });
   
    // Let them know why you sent them a bunch of docs out of the blue
    // (specially if you're sending many!)
    GmailApp.sendEmail(person.email, "New year, blank slate!",
      "Hello!\n\nI have created and shared new documents for 2018.\n\nThanks,\nSole");
  });
}

function getReplacements(data) {
  var replacements = {
    'PERSON': data.name,
    'YYYY': 2018
  };
  return replacements;
}

function personalise(str, data) {
  var out = str;
  var replacements = getReplacements(data);
 
  Object.keys(replacements).forEach(function(term) {
    out = out.replace(term, replacements[term]);
  });
  return out;
}

function personaliseDoc(doc, data) {
  var replacements = getReplacements(data);
  var body = doc.getBody();

  Object.keys(replacements).forEach(function(term) {
    body.replaceText(term, replacements[term]);
  });
}

function clone(docId) {
  var doc = DriveApp.getFileById(docId).makeCopy();
  return doc;
}

To run this, you need to select the main function from the drop down, and press the ‘play’ button to run. You’ll need to authorise the script to read and write your docs, files and email. Scary! 😛

Each template document has a few fields that will be replaced with data when running the script. For example the title in the document is “PERSON:My name 1:1 YYYY”; when run the PERSON and YYYY placeholders will become Person number one and 2018 respectively. If you want to have more fields, you need to add them to the getReplacements function. This replacement is done using a regular expression, so admittedly you could do really fancy things here (or shoot yourself in the feet, heh!).

And the personalise* functions aren’t the most DRY you’ve ever seen, but I figured that they were simple enough and I really didn’t need to refactor them to use iterators and what not. Sometimes simple enough is enough…

Regarding Apps Script, I found the distinction between the document and the file a bit tricky at the beginning. The DriveApp only cares about files and folders, and not whether they’re documents or spreadsheets or whatever. So that’s what’s used in clone(). But then to open the document you need the id of the file clone, and use that with the DocumentApp. But this is my only minor gripe, and I think once you understand, it becomes a non-issue.

The documentation is quite decent, although a few more code samples would be nice. For example, it doesn’t tell you how to send multiline messages; I figured I could use \n because I’m an old dev, but I wonder whether someone who’s not used to ‘low level C’ would have come up with that idea naturally.

I also can’t stop being amazed that they really use a floppy disk as the icon for saving. I wonder if young people know what that is; I haven’t even owned a computer with a floppy disk in more than 10 years!

I haven’t figured out how not to use their online editor, but this script was simple enough that I didn’t need to. If I did more work with App Script, I would try to figure out things such as how to version control, etc.

All in all this is quite enjoyable to use, and once you start looking at the API you wonder what else could you automate… ah, the possibilities!

Was it `from XYZ import ABC` or was it `import ABC from XYZ`?

I was writing (read: hacking together) some Python code yesterday and when today I came back to the ES6-flavoured JavaScript I’m writing as of late, I immediately got my import declarations wrong!

Instead of

// ES6
import ABC from 'xyz';

… I accidentally typed

// ES-Python?
from 'xyz' import ABC;

… which is more like the Python syntax.

I normally advocate for copying what everyone knows, i.e. ES6 designers should have copied Python, as it makes it easier for developers to pick up new languages or features.

But if you think about that, the Python syntax is pretty awkward and counter to the way English reads, unless you’re exercising some fancy stylistic choices. And code is not the right place for fancy styles, specially if you want others to be able to pick where you left.

So I’m glad ES6 designers did not copy Python in this case. Not so glad that my brain seems to prefer the awkward choice!

“*Utils” classes can be a code smell: an example

You might have heard that “*Utils” classes are a code smell.

Lots of people have written about that before, but I tend to find the reasoning a bit vague, and some of us work better with examples.

So here’s one I found recently while working on this bug: you can’t know what part of the Utils class is used when you require it, unless you do further investigation.

Case in point: if you place a method in VariousUtils.js and then import it later…

var { SomeFunction } = require('VariousUtils');

it’ll be very difficult to actually pinpoint when VariousUtils.SomeFunction was used in the code base. Because you could also do this:

var VariousUtils = require('VariousUtils');
var SomeFunction = VariousUtils.SomeFunction;

or this:

var SomeFunction = require('VariousUtils').SomeFunction;

or even something like…

var SomeFunction;
lazyRequire('VariousUtils').then((res) {
  SomeFunction = res.SomeFunction;
});

Good luck trying to write a regular expression to search for all possible variations of non-evident ways to include SomeFunction in your codebase.

You want to be able to search for things easily because you might want to refactor later. Obvious requires make this (and other code manipulation tasks) easier.

My suggestion is: if you are importing just that one function, place it on its own file.

It makes things very evident:

var SomeFunction = require('SomeFunction');

And searching in files becomes very easy as well:

grep -lr "require('SomeFunction');" *

But I have many functions and it doesn’t make sense to have one function per file! I don’t want to load all of them individually when I need them!!!!111

Then find a common pattern and create a module which doesn’t have Utils in its name. Put the individual functions on a directory, and make a module that imports and exposes them.

For example, with an `equations` module and this directory structure:

equations
  linear.js
  cubic.js
  bezier.js

You would still have to require('equations').linear or some other way of just requiring `linear` if that’s what you want (so the search is “complicated” again). But at least the module is cohesive, and it’s obvious what’s on it: equations. It would not be obvious if it had been called “MathUtils” — what kind of utilities is that? formulas? functions to normalise stuff? matrix kernels? constants? Who knows!

So: steer away from “assorted bag of tricks” modules because they’ll make you (or your colleagues) waste time (“what was in that module again?”), and you’ll eventually find yourself splitting them at some point, once they grow enough to not make any sense, with lots of mental context switching required to work on them: “ah, here’s this function for formatting text… now a function to generate UUIDs… and this one for making this low level system call… and… *brainsplosion*” 😬

An example that takes this decomposition in files to the “extreme” is lodash. Then it can generate a number of different builds thanks to its extreme modularity.

Update: Another take: write code that is easy to delete. I love it!

If using ES6 `extends`, call `super()` before accessing `this`

I am working on rewriting some code that used an ES5 “Class” helper, to use actual ES6 classes.

I soon stumbled upon a weird error in which apparently valid code would be throwing an |this| used uninitialized in A class constructor error:

class A extends B {
  constructor() {
    this.someVariable = 'some value'; // fails
  }
}

I was absolutely baffled as to why this was happening… until I found the answer in a stackoverflow post: I had to call super() before accessing this.

With that, the following works perfectly:

class A extends B {
  constructor() {
    super(); // ☜☜☜ ❗️❗️❗️
    this.someVariable = 'some value'; // works!
  }
}

Edit: filed a bug in Firefox to at least get a better error message!

Don’t force users to install node modules globally when you can avoid that

How often have you seen instructions for installing a project that are something like this?

git clone https://wherever/the/project/is
cd cloned_folder
npm install -g gulp # <-- 😱

(or, instead of gulp, any other utility that the project requires for development)

This is not a good idea. You’re installing the same version of a utility globally, and perhaps the project has not been tested with that version. Things might break or even worse, work not-so-well in a strange, undebuggable way!

Don’t do it.

A better way is to use npm scripts, which belong to the project, and specify development dependencies in package.json, so when you run npm install, it also installs devDependencies locally to the projects’ node_modules folder. This keeps everything under control and does not pollute other people’s computers global space.

So, for example, if a project was asking users to run npm install -g gulp, they should do this instead:

  1. Install gulp locally as a development dependency, saving it to package.json:
    npm install --save-dev gulp
  2. Add a new entry to the scripts array in package.json:
    "build": "gulp"
  3. Ask users to run
    npm install

    the first time. And

    npm run build

    each time they need to build the project

The reason this works is because when npm runs a script, it modifies the current PATH variable to add node_modules/.bin, which contains any binaries that you installed as part of your modules. So your computer will search for binaries starting in the .bin folder, before searching in any other default location for binaries in your computer (e.g. /usr/bin)

You can find more documentation about this behaviour in the run-script page of the npm manual. I really encourage you to read it as it has lots of good stuff and Things You Wished Had Known Before 🙂

And in case it makes things clearer, this is how a hypothetical package.json would look before:

{
  devDependencies: {}
}

And after:

{
  devDependencies: {
    "gulp": "12.34.56"
  },
  scripts: {
    "build": "gulp"
  }
}

With this, the devDependency is clearly stated on the package.json file, the project developer will be developing with that version, and will not get surprises! (and the users of the module won’t either).

Another advantage of this approach is that you are not tying the project to a specific utility.

If you decide to use grunt instead of gulp later on, you just need to do whatever changes you need (Gruntfile instead of Gulpfile, edit package.json etc), and yet users of the project will still start the build process with npm run build.

It’s invisible to them (except perhaps running npm install again)!

You’re hiding internals that they do not necessarily need to know about, so it’s a good abstraction. Seriously—you won’t even need to update your getting started guide! Good stuff.

Hope that helps!

If you want to learn even more cool npm scripts tricks, I recommend Kate’s talk and accompanying post. Actually, I just realised she explained the same thing I did, so here’s her take, haha!