Customizing the Google Spreadsheet Story Card Generator

At my current project we use a Google spreadsheet to manage our backlogs. This works really well for storing and sharing the backlog, but it’s not very good for visualizing it. So we print out the stories on cards by copying and pasting each row into a document table cell and reformatting, adding extra labels, and manually inserting priority. Well, that’s what we did the first couple of times, until I found David Vujic’s fantastic Index Card Generator for Google spreadsheets (http://davidvujic.blogspot.se/2011/06/visa-vad-du-gor-eller-dude-wheres-my.html).

Except, we have multiple backlogs in one sheet, our column names aren’t the same, and we use a different layout for the cards. Here’s how we customized David’s script!

Setting up the script

David’s spreadsheet contains everything you need (including resource links and instructions) https://docs.google.com/spreadsheet/ccc?key=0AiK_4OSo0f4LdFVOY3hXclRZcjdYQ1I2Q1VodElUdnc&hl=sv#gid=9. Make a copy of the spreadsheet, then go to tools-> Script editor:

This opens a new view with the script code. Copy the source code from this sheet into your own backlog spreadsheet, by copying the source and opening the script editor (select blank project, or just close the dialog) in your own document and pasting the code.

Setting up your spreadsheet

The card generator spreadsheet has 3 important sheets.

In order to run the script, you will need to have a sheet named Backlog, with the following columns: Id, Name, User story, How to test, Importance, Estimate:

The name and case of the columns are important, as is the name of the sheet. Update your backlog sheet to match the column names, or copy over the template sheet to your spreadsheet for testing.

The Cards sheet is generated by the script, so you don’t need to copy it over.

The Template sheet determines the formatting of the generated cards, make sure to copy that over:

Deploying the script

Now that you have copied over the setup to your own spreadsheet you need to publish the script, then update the spreadsheet (refresh the page), and finally run it for the new menu option.

To publish the script go to the script viewer tab/window and select publish:

Set a product version, then deploy, while you’re testing it’s ok to just give access to yourself, you can always redeploy later on to change the access rights. You’ll also need to redeploy every time you make changes.

Running the script

Now you’re set to run the script.

Refresh your document, and you’ll see that you have a new menu item called Story Cards, try generating some cards:

The first time you run the script you’ll be prompted to authorize the script for the Google account that you’re using with this script. Go ahead and authorize, then run create cards again. If you don’t have the Cards sheet, that will now be created, and you’ll have to run the script again. Finally you should see your cards in the Cards sheet!

Understanding the Script

Now that the script is tested and running you can start making changes. The script places data using the card template by using a grid location system. If you would like to change the way the template looks, and make modifications to the placement of the items, you’ll need to change both the template and the script. The cell values in the script are the 1 based location in the template (row,column). The index names are the actual names of the columns from the backlog sheet:

There are two functions that start the card generating process, one will generate cards for all the rows in the sheet called Backlog, the other one will generate cards for only the selected rows:

Modifying the script

Since our spreadsheet has more than one backlog, we removed the ability to create all cards from a sheet called backlog. Instead, the script now generates cards for the selected rows in the active sheet, as long as the sheet name contains the word backlog:

&lt;/p&gt;<br>
&lt;pre&gt;function createCardsFromSelectedRowsInBacklog() {<br>
if (!assertCardSheetExists()) {<br>
return;<br>
}</p>
<p>var activeSheetName = getActiveSheet().getName().toLowerCase();<br>
if (activeSheetName.indexOf("backlog") === -1) {<br>
Browser.msgBox("A Backlog sheet needs to be active when creating cards from selected rows. Please try again. (Make sure the sheet name contains the word backlog for it to be recognized as a backlog sheet)");<br>
return;<br>
}&lt;/pre&gt;<br>
&lt;p&gt;

The redesigned the template so that there was less white space, and more space for text:

We renamed and modified the column names: Estimate, Title, User story, How to test

The script now also auto calculates the order (which in our case is the inherent priority), and prints that out. This works well for us as we only use the physical cards for our sprint backlogs, so we always print the cards in the priority order. The new code is as follows:

&lt;/p&gt;<br>
&lt;p&gt;function setCardOrder(cardOrder, card) {&lt;br&gt;<br>
card.getCell(8, 1).setValue(cardOrder);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setCardName(backlogItem, card) {&lt;br&gt;<br>
var max = 49;&lt;br&gt;<br>
var storyName = backlogItem['Title'];&lt;/p&gt;<br>
&lt;p&gt;if (storyName &amp;amp;&amp;amp; storyName.length &amp;gt; max) {&lt;br&gt;<br>
storyName = storyName.substring(0, max) + '...';&lt;br&gt;<br>
}&lt;br&gt;<br>
card.getCell(2, 1).setValue(storyName);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setUserStory(backlogItem, card) {&lt;br&gt;<br>
card.getCell(4, 1).setValue(backlogItem['User Story']);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setHowToTest(backlogItem, card) {&lt;br&gt;<br>
card.getCell(6, 1).setValue(backlogItem['How to Test']);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setEstimate(backlogItem, card) {&lt;br&gt;<br>
card.getCell(7, 1).setValue("Estimate:" + backlogItem['Estimate'] );&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;

The complete modified script can be expanded below:

&lt;br&gt;<br>
function getTemplateArea() {&lt;br&gt;<br>
return "A1:B8";&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setCardOrder(cardOrder, card) {&lt;br&gt;<br>
card.getCell(8, 1).setValue(cardOrder);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setCardName(backlogItem, card) {&lt;br&gt;<br>
var max = 49;&lt;br&gt;<br>
var storyName = backlogItem['Title'];&lt;/p&gt;<br>
&lt;p&gt;if (storyName &amp;amp;&amp;amp; storyName.length &amp;gt; max) {&lt;br&gt;<br>
storyName = storyName.substring(0, max) + '...';&lt;br&gt;<br>
}&lt;br&gt;<br>
card.getCell(2, 1).setValue(storyName);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setUserStory(backlogItem, card) {&lt;br&gt;<br>
card.getCell(4, 1).setValue(backlogItem['User Story']);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setHowToTest(backlogItem, card) {&lt;br&gt;<br>
card.getCell(6, 1).setValue(backlogItem['How to Test']);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function setEstimate(backlogItem, card) {&lt;br&gt;<br>
card.getCell(7, 1).setValue("Estimate:" + backlogItem['Estimate'] );&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getTemplateStartColumn() {&lt;br&gt;<br>
return getTemplateArea().substring(0,1);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getTemplateStartRow() {&lt;br&gt;<br>
return parseInt(getTemplateArea().substring(1,2), 10);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getTemplateLastColumn() {&lt;br&gt;<br>
return getTemplateArea().substring(3,4);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getTemplateLastRow() {&lt;br&gt;<br>
return parseInt(getTemplateArea().substring(4), 10);&lt;br&gt;<br>
}&lt;br&gt;<br>
// END: Template functions&lt;/p&gt;<br>
&lt;p&gt;// START: Get sheets&lt;br&gt;<br>
function getSpreadsheet() {&lt;br&gt;<br>
return SpreadsheetApp.getActiveSpreadsheet();&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getActiveSheet() {&lt;br&gt;<br>
return getSpreadsheet().getActiveSheet();&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getBacklogSheet() {&lt;br&gt;<br>
return getSpreadsheet().getSheetByName("Backlog");&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getTemplateSheet() {&lt;br&gt;<br>
return getSpreadsheet().getSheetByName("Template");&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getCardSheet() {&lt;br&gt;<br>
return getSpreadsheet().getSheetByName("Cards");&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;function getPreparedCardSheet(template, numberOfItems, numberOfRows) {&lt;br&gt;<br>
var rowsNeeded = numberOfItems * numberOfRows;&lt;/p&gt;<br>
&lt;p&gt;var sheet = getCardSheet();&lt;br&gt;<br>
sheet.clear();&lt;/p&gt;<br>
&lt;p&gt;setColumnWidthTo(sheet, template);&lt;/p&gt;<br>
&lt;p&gt;var rows = sheet.getMaxRows();&lt;/p&gt;<br>
&lt;p&gt;if (rows &amp;lt; rowsNeeded) {<br>
sheet.insertRows(1, (rowsNeeded - rows));<br>
}</p>
<p>setRowHeightTo(sheet, numberOfRows, numberOfItems);</p>
<p>return sheet;<br>
}<br>
// END: Get sheets</p>
<p>// START: Get range within sheets<br>
function getTemplateRange() {<br>
return getTemplateSheet().getRange(getTemplateArea());<br>
}</p>
<p>function getHeadersRange(backlog) {<br>
return backlog.getRange(1, 1, 1, backlog.getLastColumn());<br>
}</p>
<p>function getItemsRange(backlog) {<br>
var numRows = backlog.getLastRow() - 1;</p>
<p>return backlog.getRange(2, 1, numRows, backlog.getLastColumn());<br>
}</p>
<p>function getSelectedItemsRange(backlog) {<br>
var range = getSpreadsheet().getActiveRange();<br>
var startRow = range.getRowIndex();<br>
var rows = range.getNumRows();</p>
<p>if (startRow &amp;lt; 2 ) {<br>
startRow = 2;<br>
rows = (rows &amp;gt; 1 ? rows-1 : rows);&lt;br&gt;<br>
}&lt;/p&gt;<br>
&lt;p&gt;return backlog.getRange(startRow, 1, rows, backlog.getLastColumn());&lt;br&gt;<br>
}&lt;br&gt;<br>
// END: Get range within sheets&lt;/p&gt;<br>
&lt;p&gt;function setRowHeightTo(cardSheet, numberOfRows, numberOfItems) {&lt;br&gt;<br>
var templateSheet = getTemplateSheet();&lt;/p&gt;<br>
&lt;p&gt;for (var i = 0; i &amp;lt; numberOfItems; i++) {<br>
for (var j = 1; j &amp;lt; (numberOfRows+1); j++) {<br>
var currentRow = (i*numberOfRows)+j;<br>
var currentHeight = templateSheet.getRowHeight(j);<br>
cardSheet.setRowHeight(currentRow, currentHeight);<br>
}<br>
}<br>
}</p>
<p>function setColumnWidthTo(cardSheet, templateRange) {<br>
var templateSheet = getTemplateSheet();<br>
var max = templateRange.getLastColumn() + 1;</p>
<p>for (var i = 1; i &amp;lt; max; i++) {<br>
var currentWidth = templateSheet.getColumnWidth(i);<br>
cardSheet.setColumnWidth(i, currentWidth);<br>
}<br>
}</p>
<p>/* Get backlog items as objects with property name and values from the backlog. */<br>
function getBacklogItems() {<br>
var backlog = getActiveSheet();</p>
<p>var rowsRange = getSelectedItemsRange(backlog);<br>
var rows = rowsRange.getValues();<br>
var headers = getHeadersRange(backlog).getValues()[0];</p>
<p>var backlogItems = [];</p>
<p>for (var i = 0; i &amp;lt; rows.length; i++) {<br>
var backlogItem = {};</p>
<p>for (var j = 0; j &amp;lt; rows[i].length; j++) {<br>
backlogItem[headers[j]] = rows[i][j];<br>
}</p>
<p>backlogItems.push(backlogItem);<br>
}</p>
<p>return backlogItems;<br>
}</p>
<p>function assertCardSheetExists() {<br>
if (getCardSheet() == null) {<br>
getSpreadsheet().insertSheet("Cards", 0);<br>
Browser.msgBox("The 'Cards' sheet was missing and has now been added. Please try again.");<br>
return false;<br>
}</p>
<p>return true;<br>
}</p>
<p>function createCardsFromSelectedRowsInBacklog() {<br>
if (!assertCardSheetExists()) {<br>
return;<br>
}</p>
<p>var activeSheetName = getActiveSheet().getName().toLowerCase();<br>
if (activeSheetName.indexOf("backlog") === -1) {<br>
Browser.msgBox("A Backlog sheet needs to be active when creating cards from selected rows. Please try again. (Make sure the sheet name contains the word backlog for it to be recognized as a backlog sheet)");<br>
return;<br>
}</p>
<p>var backlogItems = getBacklogItems();<br>
createCards(backlogItems);<br>
}</p>
<p>function createCards(backlogItems) {<br>
var numberOfRows = getTemplateLastRow();<br>
var template = getTemplateRange();<br>
var cardSheet = getPreparedCardSheet(template, backlogItems.length, numberOfRows);</p>
<p>var startRow = getTemplateStartRow();<br>
var lastRow = getTemplateLastRow();<br>
var startColumn = getTemplateStartColumn();<br>
var lastColumn = getTemplateLastColumn();</p>
<p>for (var i = 0; i &amp;lt; backlogItems.length; i++) {<br>
var rangeVal = startColumn + startRow + ":" + lastColumn + lastRow;<br>
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;var card = cardSheet.getRange(rangeVal);<br>
template.copyTo(card);</p>
<p>setCardOrder(i+1, card);<br>
setCardName(backlogItems[i], card);<br>
setUserStory(backlogItems[i], card);<br>
setHowToTest(backlogItems[i], card);<br>
setEstimate(backlogItems[i], card);</p>
<p>startRow += numberOfRows;<br>
lastRow += numberOfRows;<br>
}</p>
<p>Browser.msgBox("Story cards successfully created in the sheet named 'Cards'");<br>
}</p>
<p>/* Will add a Cards menu. Runs when the spreadsheet is loaded. */<br>
function onOpen() {<br>
var sheet = getSpreadsheet();<br>
var menuEntries = [ {name: "Create cards from selected rows", functionName: "createCardsFromSelectedRowsInBacklog"} ];</p>
<p>sheet.addMenu("Story Cards", menuEntries);<br>
}</p>
<p>&lt;/p&gt;&lt;pre&gt;

That’s how you do it!

You can find the modified spreadsheet and script here: https://docs.google.com/spreadsheet/ccc?key=0At-EEnHR4tuzdFFuV1c4U3JEcXhzYTNpdzJWWFFjYWc&usp=sharing

Feel free to try it out and extend it!

Get in touch via my homepage if you have questions or comments!

5 responses on “Customizing the Google Spreadsheet Story Card Generator

    1. Oh no, I didn’t notice the iPad autocorrect before sending 🙂 “IDG has unfortunately removed their entire blog section…” is what I meant to write.

  1. Hey, I just came across your template. Will be using that for one of our projects, thanks for providing the template!

Comments are closed.