Google Apps Script withSuccessHandler is running before function fully executes - google-apps-script

I have two files in an Apps Script project. One is a .gs file acting as a "server" and one is a .html file containing JavaScript as per Google's Apps Script guidelines.
Everything has been going swimmingly for the first 40 hours of development on this project. I have the following line of code outside of any function, in between two tags in the .html file:
google.script.run.withSuccessHandler(setSheetData).getSheetData();
Documentation: https://developers.google.com/apps-script/guides/html/reference/run#withSuccessHandler(Function)
According to the documentation, getSheetData() should first execute in the .gs file, and return a value that is then passed into setSheetData which exists in the .html file.
Server file:
function getSheetData() {
var ss = SpreadsheetApp.getActive();
var activeSheet = ss.getActiveSheet();
var sheetName = activeSheet.getName();
var sheetVals = activeSheet.getDataRange().getValues();
return [sheetName, sheetVals];
}
Html file:
function setSheetData(data) {
alert(data);
sheetName = data[0];
sheetData = data[1];
headers = sheetData[0];
document.getElementById('sheetLook').innerHTML = 'Looking at sheet: ' + sheetName;
}
How I know it is a matter of execution speed:
Currently the alert() call just prints out null. The sheet it is drawing from contains 4 rows of data. However, all other things remaining the same, if I simply am looking at a sheet with 0-1 rows of data, it correctly alerts the entire data vals.
Inside of getSheetData() if I add Logger.log(sheetVals) it correctly logs the entire sheet's data regardless of size. The issue is that the successhandler is executing before it has time to evaluate.

Possible cause(s):
Illegal return values in rows 3-4 of data
No active sheet is present.
Solution:
Avoid returning illegal values like Date objects. Or JSON.stringify() them before returning to client.
getSheetByName or number instead of getting it by activeness.
References:
google.script.run § myFunction § return

Related

appendRow() adds blank row in google sheets (app script)

I've setup a google app script that would be triggered from an external system. This script would fetch the details from the third party system and add them to google sheet row.
function doPost(request) {
try{
var jsonString = request.postData.getDataAsString(); //get the request from KF as JSON String
setLog("\n postData*********************"+jsonString+"************************************* \n");
setLog("before the row append");
ss.appendRow([jsonString["Name"], jsonString["Age"], jsonString["Contact"]]);
setLog("After the row append");
var returnJson = '{"status": "success"}';
//used to send the return value to the calling function
setLog("/n returnJson****************************"+returnJson+"************************************* /n")
return ContentService.createTextOutput(returnJson).setMimeType(ContentService.MimeType.JSON);
}
There's absolutely no errors or warnings, but somehow it keeps adding the blank rows into the sheet.
Note: setLog() is a function where I print the values into google doc for debugging.
Maybe the reason your script is not working has to do with the value of jsonString.
I could not find any reference to request.postData.getDataAsString() inside GAS Documentation, so maybe you are trying to call a method on an object which does not support it, which would not raise an Error, but would return undefined.
One quick way to debug this would be to LOG the value (using your custom function or Logger.log(jsonString)) BEFORE you call .appendRow(). Then, you can verify if your variable has the value you expect it to have.
On the other hand, my suggestion is to use this method:
var jsonString = JSON.parse(request.postData.contents) //Gets the content of your request, then parses it
This method is present in the Documentation, and has been consistently working on all of my projects.
I think you should sort the coulmns with google app script. Write this code after ss.appendRow. The column will be sorted and all blank rows gets down.
// Sorts the sheet by the first column, ascending
ss.sort(1)
or if errors try this one also
var fl = SpreadsheetApp.getActiveSpreadsheet();
var sheet = fl.getSheets()[0];
fl.sort(1)

Google Script timeout after 6 min

I want to list all the folderName and their folderID present in a team Drive(more than 3000 folders). I am using speedsheet and running following code in script-
function listFilesInFolder(folderName) {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(['Name','File-Id']);
//change the folder ID below to reflect your folder's ID (look in the URL when you're in your folder)
var folder = DriveApp.getFolderById('folder ID');
var contents = folder.getFolders();
var cnt = 0;
var folder;
while (contents.hasNext()) {
var folder = contents.next();
cnt++;
data = [
folder.getName(),
folder.getId(),
];
sheet.appendRow(data);
};
}
But this is getting Error Exceeded maximum execution time which is 6 min by default.
I tried adding triggers from script app, but after triggering it get start from beginning and script still ends after 6min.
How to add a triggers which starts from where it left?
Answer:
The slow part of this script is the repeated call to sheet.appendRow(). You can speed this up by pushing the results to an array and setting the values at the end of the loop, rather than appending a row on each iteration of the while loop.
More Information:
Using the built-in services such as SpreadsheetApp often can be slow when making many changes to a sheet in a short space of time. You can combat this by minimising the number of calls to the built-in Apps Script services as possible, relying on pure JavaScript to do your processing.
Code Change:
function listFilesInFolder(folderName) {
const sheet = SpreadsheetApp.getActiveSheet()
//change the folder ID below to reflect your folder's ID (look in the URL when you're in your folder)
let folder = DriveApp.getFolderById('')
let contents = folder.getFolders()
let cnt = 0
let data = [['Name','File-Id']]
while (contents.hasNext()) {
folder = contents.next()
cnt++
data.push([
folder.getName(),
folder.getId(),
])
}
sheet.insertRows(sheet.getMaxRows(), data.length)
sheet.getRange(2, 1, data.length, 2).setValues(data)
}
Code changes:
data is declared as an array initialised with the header row, as opposed to appending it directly to the sheet
On each iteration of the loop, the current folder's name and ID is appended to the data array as a new row of data.
After all folders have been looped through, the number of rows in the sheet is extended by the number of rows in data so to not hit an out of bounds error
All rows inside data are added to the sheet using setValues().
In my test environment, I had the following set up:
Drive folder containing 3424 folders
Using the appendRow() method inside the while loop, execution took 1105.256 seconds (or 18 minutes)
Using push() with the .setValues() method outside the loop, execution took 4.478 seconds.
References:
Class Range - setValues() | Apps Script | Google Developers

Script to open a long list of URLs one at a time every 5 seconds and then open the next URL

I have a google spreadsheet with a large number of API Urls.
They look like this => http://oasis.caiso.com/oasisapi/SingleZip?resultformat=6&queryname=PRC_LMP&version=1&startdatetime=20160101T08:00-0000&enddatetime=20160103T08:00-0000&market_run_id=DAM&grp_type=ALL
The database I am drawing from limits requests to one every 5 seconds.
When you follow the link it will download a zip file with cvs files.
I would like to write a script that will follow a URL, wait 6 seconds and then move on to the next URL on the list.
I would like it to stop when it gets to the last URL
I am imagining that I would need to use a "when" loop, but I cannot figure out how to install a wait period, or how to get it to open the URL.
HELP!!!!
I tried a batch URL follow, which failed because of the timing issue.
I began to write the When loop, but I am totally stuck.
I would like to run through the huge list of links fully once. To date I cannot make anything work.
function flink(){
var app = spreasheetapp
//access the current open sheet
var activesheet = app.getactivespreadsheet().getactivesheet()
var activecell= activesheet.getrange(11,11).openurl
//I am getting totally stuck here
I have tried using an iterator but I have no idea how to add the time delay and then I cannot seem to get the syntax for the iterator correct.
To access a url from AppsScript, you can use UrlFetchApp.fetch(url).
To force the script to wait for a certain amount of time, you can use Utilities.sleep(milliseconds).
References:
https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetch(String)
https://developers.google.com/apps-script/reference/utilities/utilities#sleepmilliseconds
I came up with this code that accesses each URL in the sheet. I added some comments so you can see what the script is doing step by step:
function accessURLs() {
// Copy urls
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("Links up to today");
// Copy the contents of the url to another column removing the formulas (your script will appreciate this)
sheet.getRange("C3:C").copyTo(sheet.getRange("D3:D"), {contentsOnly:true});
var column = 4; // URLs are in column 4
var row = 3; // URLs start at row 3
var url = sheet.getRange(row, column).getValue();
var responses = []
// Loop through all urls in the sheet until it finds a cell that is blank (no more urls left)
do {
var options = {
muteHttpExceptions: true
}
var response = UrlFetchApp.fetch(url, options); // Script makes a request to the url
responses.push(response); // The latest response is added to the responses array
Utilities.sleep(6000); // The script stops for 6 seconds
row++;
url = sheet.getRange(row, column).getValue();
} while (url != ""); // Cell is not blank
}
Take into account that if we are to access ~1400 URLs and the script stops for 6 seconds after each fetch, it will take more than 2 hours to access all URLs.
Also, take into account that this script is just getting the data coming from the URL requests (it's stored in the variable responses), but it is not doing anything else. Depending on what you want to do with this data, you might want to add some extra stuff there.
I hope this is of any help.

How to trigger Google Apps script function based on insert row via api

I have a Google Sheet with 5 columns (First Name, Address, SKU, Quote, Status).
I have an apps script function (createQuote) which looks at the above variable's values from google sheet row and create a google document quote replacing the variables to values.
I use Zapier to insert row into my above google sheet.
What am struggling with-:
I need a way to trigger my createQuote function right when a new row is inserted via zapier (Google Sheet API call).
I tried playing with triggers but couldn't make it, any help is appreciated.
thank you
here is the code for my function-
function quoteCreator(){
docTemplate = "googledocidgoeshere"
docName = "Proposal"
var sheet = SpreadsheetApp.getActive().getSheetByName("Main")
var values = sheet.getDataRange().getValues()
var full_name = values[1][0]
var copyId = DriveApp.getFileById(docTemplate).makeCopy(docName+" for "+full_name).getId()
// Open the temporary document
var copyDoc = DocumentApp.openById(copyId);
// Get the document’s body section
var copyBody = copyDoc.getActiveSection();
// Replace place holder keys/tags,
copyBody.replaceText("keyFullName", full_name);
copyDoc.saveAndClose();
// Convert temporary document to PDF by using the getAs blob conversion
var pdf = DriveApp.getFileById(copyId).getAs("application/pdf");
// put the link of created quote in the quote column
var url = DocumentApp.openById(copyId).getUrl()
var last = sheet.getRange(2, 7, 1, 1).setValue(url)
}
Note-: I haven't put the loop yet in above, i'll do that once it starts working as per my requirements.
Changes made via Sheets API or Apps Script do not fire onEdit triggers. I give two workarounds for this.
Web app
Have whatever process updates the sheet also send a GET or POST request to your script, deployed as a web application. As an example, a GET version might access https://script.google.com/.../exec?run=quoteCreator
function doGet(e) {
if (e.parameter.run == "quoteCreator") {
quoteCreator();
return ContentService.createTextOutput("Quote updated");
}
else {
return ContentService.createTextOutput("Unrecognized command");
}
}
The web application should be published in a way that makes it possible for your other process to do the above; usually this means "everyone, even anonymous". If security is an issue, adding a token parameter may help, e.g., the URL would have &token=myToken where myToken is a string that the webapp will check using e.parameter.token.
GET method is used for illustration here, you may find that POST makes more sense for this operation.
Important: when execution is triggered by a GET or POST request, the methods getActive... are not available. You'll need to open any spreadsheets you need using their Id or URL (see openById, openByUrl).
Timed trigger
Have a function running on time intervals (say, every 5 minutes) that checks the number of rows in the sheet and fires quoteCreator if needed. The function checkNewRows stores the number of nonempty rows in Script Properties, so changes can be detected.
function checkNewRows() {
var sp = PropertiesService.getScriptProperties();
var oldRows = sp.getProperty("rows") || 0;
var newRows = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Main").getLastRow();
if (newRows > oldRows) {
sp.setProperty("rows", newRows);
quoteCreator();
}
}

Google Docs Custom Refresh Script

I'm trying to write a script that allows me to execute commands as soon as Google finishes making calculations (i.e. I'm trying to add to a script to Google docs that imitates some VBA "Calculate" functionalities).
The script is conceived to work by converting a range into a string and looking for the substring "Loading..." (or "#VALUE!" or "#N/A") in that string. The "while" loop is supposed to sleep until the unwanted substrings are no longer found in the string.
I'm using the following spreadsheet as a sandbox, and the code seems to work okay in the sandbox just searching for "Loading...":
https://docs.google.com/spreadsheet/ccc?key=0AkK50_KKCI_pdHJvQXdnTmpiOWM4Rk5PV2k5OUNudVE#gid=0
In other contexts, however, I have cells whose values may return as "#VALUE!" or "#N/A" for reasons other than the fact that Google is still loading/thinking/calculating. What's the way around this?
function onEdit() {
Refresh();
};
function Refresh () {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
// set the range wherever you want to make sure loading is done
var range = sheet.getRange('A:A')
var values = range.getValues();
var string = values.toString();
var loading = "Loading";
do
{
var randomWait = Math.floor(Math.random()*100+50);
Utilities.sleep(randomWait);
}
while (string.search(loading) ==! null);
range.copyTo(sheet2.getRange('A1'), {contentsOnly:true});
customMsgBox();
};
function customMsgBox() {
Browser.msgBox("Data refreshed.");
};
rather than using a while loop to "sleep" you should add an event handler to your document which captures the update/refresh event and then runs whatever math/processing you need.
Here's a good place to start reading about events: https://developers.google.com/apps-script/understanding_events
but if you search the api documents for eventhandler you can get some example code fast...