Cache custom function result between spreadsheet opens - google-apps-script

I have a Google spreadsheet that has it's data loaded using a custom function.
The custom function calls a REST API, transforms the data and displays it.
The problem is that every time I open the doc, the function is called and all the REST API calls go out.
I need to be able to manually trigger service calls to refresh data, and in between show the data from last time.
How can I best achieve that?

Short answer
Instead of calling your function from a formula (custom function) call it from:
The script editor
A custom menu
Explanation
At this time custom functions are recalculated when the spreadsheet is open and when any of its arguments changes its values. To run the function at will use the script editor play or debug buttons or a custom menu. Also you could use other triggers. Choose the one that best suit your needs.
If you still want to cache the result of calling the corresponding REST API use the Class Cache.
References
Overview of Google Apps Script
Custom functions in Google Sheets
Cache Service

Related

Centralized script for multiple spreadsheets

I have created a Notification script and would like to use it for multiple spreadsheets (produced by Forms). I would like to have a centralized script and referencing it on my multiple spreadsheets in order to simplify maintenance.
I have tried to use the SpreadsheetApp.openById() like below but looks like this feature has been disabled for security reasons and is not supported anymore.
function append()
{
SpreadsheetApp.openById("1xdePF..........................");
}
Any idea on how to use the same script for multiple spreadsheets?
The documentation specifies that openById opens the spreadsheet with the given ID.
You cannot use this method to open a script.
To open an Apps Script, go to https://script.google.com/home/my and clock on the script of your choice.
For bound scripts, you open the document to which the script is bound and go to Tools->Script Editor
Preface
This answer is supplementary to ziganotschka's, since you indeed cannot access the script bound to a document by opening said document with openById(). The answer, instead, covers other issues you had and suggests additional ways to solve your task.
Problem
When trying to use openById() you receive an error message of the following structure:
Exception: Document [doc id here] is missing (perhaps it was deleted, or maybe you don't have read access?)
You mentioned in comments that the id is 58 chars (I bit it is 57) and obtained from "properties", which explains the error - there is no spreadsheet with such an id, because file id and script id you extracted are not the same thing. If you ever need to extract current id programmatically (here it is assumed to be called from a script bound to spreadsheet, but other services have similar methods), you can call getActiveSpreadsheet() -> getId().
Solutions
You stated that you need a maintenance script, so how about creating a standalone script project that is deployed in a way any document can access:
As a library
Any script that has a saved version can be a library that can be used by other scripts by adding its id to the list accessible from Resources->Libraries menu.
As a Web App
Any script that has a doGet, doPost function (or both) can be deployed as a Web App, essentially exposing it to the net. Since you said that the spreadsheets are "produced" by Forms, and you created a "notification" script, I assume you are interested in a FormSubmit event. When you deploy as a Web App, you get a url (don't forget to choose an appropriate permission).
After that, it is only a matter of making sure that:
each Form has an installable onFormSubmit trigger (you can instlall it via [please see refs for docs] ScriptApp.newTrigger('callback name').forForm('form ref').onFormSubmit()).
The callback for the trigger calls the Web App url via UrlFetchApp.fetch() with necessary data (like spreadsheet id or any other info) as query (if using doGet) or request body (if using doPost).
The data you need will be available in the event object constructed on hit to doGet or doPost [note to avoid common misconception: you can't debug event objects in the editor, it can only be done live].
Reference
getActiveSpreadsheet() docs
getId() docs
getFileById() docs
Standalone scripts guide
Bound scripts guide
Libraries guide
Form submit event reference
new Trigger() docs
UrlFetchApp.fetch() docs
doGet / doPost event object docs

Can you do something on a Google-Apps-Script fail?

Is there a way to have an automatic 'back-up' plan if my Google Apps Script fails? I'm thinking something along the lines of a python Try / Except.
For example, I am building a function to create a calendar event from a form (script is attached to the spreadsheet the form is linked to). The script creates an event number for the new form submission (storing it in the spreadsheet) and then makes the calendar event. If my function fails in making the event, there is a new event number in the spreadsheet, but no event in the calendar.
Does Google Scripts have a pre-built way to fail gracefully?
Or, is there a way to build an outer function that runs the original and has the graceful fail as (e.g.) the else of an if-then-else?
Apps Script is based on Javascript, so you can use the try...catch functionality.
If you are calling functions on your Apps Script back-end from your front-end you can also use the withFailureHandler to handle a failed execution.

how to share same app-script between multiple (different) spreadsheets in google spreadsheets

First of all I am after 3hours of reading docs about google cloud, publishing, projects and so on. After many tries i realized that i am missing something so here is my question.
I have two spreadsheets, lets, call it "prices" and "costs". What i want to achieve is that they share same app scripts and if I change one script, since it is shared by both it will automatically change in secons.
So i did create an app script that returns a string (just for simplicity) in a cell and called it STRINGFUNCTION(); Is is created in PRICES spreadsheet. My goal is to have it working in COSTS file without typing it manually.
I expected that if i click Resources > Cloud Platform Project and add both app scripts from both spreadsheets to the same project it will work automatically. Well, it wont - if i write in a cell =STRINGFUNCTION() in PRICES it works fine, and in COSTS - it says that function in not known.
How can I achieve that so it works between my both files and they share same function if they are both in same project?
You can do this by putting your code in a standalone script and use it as a Library.
https://developers.google.com/apps-script/guides/libraries
Here's how to do this:
Go to https://script.google.com/ and create a new project
Replace the code with the code for your custom functions and save
Click "Untitled Project" and give it a name to use for accessing the library
Click the blue Deploy button and choose New Deployment
Click the gear beside Select type and choose Library
Enter a description and click Deploy (this is what makes your Library available from other scripts)
Go to Project Settings and copy the script ID
Go back to your Spreadsheet, open the script for your sheet, click + beside libraries and paste the script ID and click Add
Remove the code for the custom functions if necessary and you're going to create functions to pass through the library functions
If your function is called STRINGFUNCTION(), create a function in the local apps script like this:
function STRINGFUNCTION(parameter) {
return libraryName.STRINGFUNCTION(parameter);
}
Do that for each function you want to use from the Library and save your code. You will need to authorize permission for the script if you haven't already. Now the custom functions should be available in your spreadsheet. Copy and paste this "pass through" script to each sheet where you want to be able to access the custom functions from the Library.
Custom functions inside a Library cannot be called directly from a sheet. You only have to set up this local script to pass through the functions, once. Now if you update the script in the Library, the updated functions will be available to the sheet. You will need to do the deploy set each time you make a change to the Library to publish the changes. If the changes aren't working in the sheet, click on the Library in the local script and make sure the version is Head or the latest version you published.
You can only bind a Google Apps script file to one document at a time. Apps script doesn't allow you to edit the contents of a .gs file on the cloud from inside another Apps script file, as trying to fetch:
var data = DriveApp.getFileById('script-id').getAs('application/vnd.google-apps.script');
will return the error:
No item with the given ID could be found, or you do not have permission to access it.
You could however bind the script to one spreadsheet, for example say the 'COSTS' spreadsheet, and create a second sheet within the spreadsheet for 'PRICES' with all the relevant function calls. In the separate 'PRICES' spreadsheet you could then use the build-in IMPORTRANGE formula to get the data from the 'PRICES' range from the first spreadsheet that has the bound script.

Why Isn't My Spreadsheet Changing From Input Values of An Outside Source?

So I am trying to use an outside source (Zapier) to input values into my spreadsheet. These input values are then "transposed (formula wise)" into my spreadsheet to fit the cell coordinates with which they are to align.
I have the spreadsheet set to run 'onEdit' and when these incoming values arrive, it is supposed to cause the rest of the spreadsheet to change, but the function is not running.
However, if I were to edit the spreadsheet 'manually,' the onEdit function runs perfectly.
So why then would the spreadsheet not be running the function, when the outside source brings its input values?
UPDATE:
So I discovered that if I manually authorize an 'onChange' installable trigger, it will work. But if I create a copy of the same exact spreadsheet, the installable trigger will not exist in the copy. The copy needs to have the trigger without me having to do it manually. So I am trying to create a code inside of Google Script Editor that will either allow me to use the onChange function or install the onChange function in the Developer Hub. Any thoughts? Here is the code I tried but did not work:
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("My
Sheet")
var ssid = "My SpreadSheet ID";
ScriptApp.newTrigger('My Sheet)
.forSpreadsheet(ss.getId())
.onChange()
.create();
myFunction()
{
If there is an alternative for the onChange function, then I'm all ears. I just need a function that can run itself in my copies.
As a part of a collaborative effort, let me clarify the Zapier part (this answer does not concern the copy part).
Part 1. Zapier setup
Assuming you have a third-party application that you pull data from (btw, since you decided to use apps script, isn't it easier to drop the middleman like Zapier and connect to the 3P app API – if it has one, ofc – directly?), you created a Catch Hook and a POST Action.
The POST Action setup contains several fields:
URL field - this is where your /exec URL goes (WebApp is deployed via Publish->Deploy as a WebApp). After you deploy your script as a WebApp, you will get a URL that users and scripts can make requests to (it is always of this format https://script.google.com/macros/s/{yourProjectId}/exec - with some slight diff. due to access permissions). To avoid permissions issue, set the Who has access to the app option to anyone or anyone, even anonymous (otherwise, you'll have to devise auth handling).
Payload Type field is irrelevant here, but I suggest using JSON.
Data field is required if you chose the POST Action and should contain key-value pairs of data you would like to transmit via Zapier (the data will be available in parameter/parameters property of the event object).
Part 2. WebApp setup
Published WebApps should have either a doGet() or doPost() function to be able to receive and process requests (or both). Each of them accepts one special argument, which is constructed each time a request to the WebApp is made – an event object.
The event object will contain all the data that you sent from Zapier. You can then use this data to conditionally trigger different functions, pass data to handlers, etc. So, instead of relying on triggers, you can create a function that is called inside the doGet / doPost that will a) populate your target sheet with new values; b) do anything else after that, thus acting as an analogue of onEdit / onChange.
Useful links
Event object structure;
Passing event objects around;
Creating triggers on other documents;

Can a Google Spreadsheet Apps Script library contain a user dialog?

Are there restrictions on what may be in an Apps Script library to be used by Google Spreadsheets? Specifically, can a library include an HTML dialog?
I've created a spreadsheet script that adds a menu item to present the user with a dialog box. It uses
HtmlService.createHtmlOutputFromFile('mappingForm').setSandboxMode(HtmlService.SandboxMode.IFRAME)
as described in https://developers.google.com/apps-script/guides/html/communication. The HTML file includes HTML, CSS and JavaScript with jQuery. It uses google.script.run to populate the dialog with data from the spreadsheet and to submit a form to it.
This all works fine in the original spreadsheet.
I need multiple spreadsheets to use this same code, however, so I'm trying to follow the general idea of Google Spreadsheet Scripts shared across spreadsheets (not libraries) to have a master script with a spreadsheet template and multiple copies.
I followed the directions at https://developers.google.com/apps-script/guide_libraries to create a library from the original spreadsheet. When another spreadsheet uses the library, I'm able to get the dialog to appear, but all calls back to the server (either to populate the dialog or to submit a form) fail with an error caught browser-side by the google.script.run.withFailureHandler as an Error object with properties:
message: "We're sorry, a server error occurred. Please wait a bit and try again."
name: "ScriptError"
I've placed Logger calls in the apps script to see if the server-side functions are being called but none of them are being hit. The script editor's Execution Transcript shows:
[14-12-27 19:38:05:340 PST] Starting execution
[14-12-27 19:38:05:372 PST] Execution failed: We're sorry, a server error occurred. Please wait a bit and try again. [0.0 seconds total runtime]
The client is making the call, but something is failing before it reaches the spreadsheet script.
This makes me wonder whether
I need to do something differently for the code to work as a library.
Libraries can't have dialogs.
There's a server bug.
Thanks in advance for any suggestions.
I was able to have a working library containing an HTML dialog by doing the following.
Move the script and HTML files from the original spreadsheet to a standalone script project. Take note of the library's Project key in the Info tab of File > Project properties.... It will be needed by any spreadsheet that intends to use the library.
If the standalone script project is to used by others, click its Share button to make it shareable to anyone with a link, otherwise it will silently fail for them.
If the HTML dialog needs to call back to a library function (to get or submit data), the library function must be present in the spreadsheet that uses the library, or you'll get an error message in the browser's JavaScript console.
In the spreadsheet that uses the library: Tools > Script editor... Click Resources > Libraries.... In the "Included Libraries" dialog box, enter the standalone project's key in the Find a Library text box, click Select, then choose the appropriate Version, change the Identifier, if necessary, and Save. The Identifier value creates an object of the same name for use by the spreadsheet's script to call library functions. In my case, it is SignupFormResponsesSheet.
In the same Script Editor's code editor, add wrapper functions that call library functions, including any that will be called back from the HTML dialog. My library has an onOpen() which creates two menu items to show HTML dialogs, so I added
function onOpen() {
SignupFormResponsesSheet.onOpen();
}
function showMappingForm() {
SignupFormResponsesSheet.showMappingForm();
}
function showSubmitForm() {
SignupFormResponsesSheet.showSubmitForm();
}
My HTML dialog has a number of callbacks to get and submit data, so rather than writing a wrapper function for each, I added one function to cover all of them by taking advantage of the way Apps Script treats a library as an object containing functions. The first argument is a string naming the library function to call. Any additional arguments are passed to the library function.
function runSignupFormResponseFunction(funcName, varargs) {
return SignupFormResponsesSheet[funcName].apply(this,
Array.prototype.slice.call(arguments, 1));
}
Because of the restriction identified in step 3 above, the JavaScript in the HTML dialog uses google.script.run to call the runSignupFormResponseFunction whenever it needs to get or submit data. For example, it has two lists that are dynamically populated with server data from the library's getRangeLabels and getColumnExamples functions (and one must be populated before the other), so the code is
google.script.run
.withFailureHandler(showError)
.withSuccessHandler(function(ranges) {
loadRanges(ranges);
// once ranges are loaded, load columns
google.script.run
.withSuccessHandler(loadColumns)
.withFailureHandler(showError)
.runSignupFormResponseFunction("getColumnExamples");
})
.runSignupFormResponseFunction("getRangeLabels");
This worked for me today. I hope it works for others who may find this question.