Trouble deploying sheets addon - google-apps-script

Our website spits out CSV files that we use as event rosters, but they include way too much data. There's no feasible way to change what's included in the export, so an admin assistant must edit & format for print. It's repetitive and time consuming, so I figured it's the perfect time to learn Google Apps Script.
Thanks to the incredible knowledge shared here on stack overflow, a total noob like me can cobble together a script that does what I need! Just by using snippets from other answers, I was able to automate:
Delete unwanted & empty columns
Rename & auto-resize columns
Sort by the last name column
Generate print-ready PDF that saves in the same Drive directory.
But now I'm having trouble testing and deploying the script as an addon so my co-workers can use it. When I run a "test as addon" the sheet opens, but nothing happens. I've tried all the variables for installation config and searched for others having the same trouble, but can't find anything so I think the problem is prbly somewhere on my end - script or user error.
Once I get it to test correctly, I'm not entirely sure about how to correctly deploy the addon to our domain and get all the permissions, etc setup correctly. I've read up and now I feel more confused than ever! So two questions:
What's wrong w/ my testing?
Once it tests successfully, what's the easiest way I can let all of our domain's apps users utilize the script?
Here's the script:
function expCalc() {
DeleteColumns();
RemoveEmptyColumns();
RenameColumns();
ResizeColumns();
Sort();
SavePDF();
}
//delete unwanted columns
function DeleteColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var dataRange = sheet.getRange("A1:AH200");
var data = sheet.getRange("A1:AH200");
var values = data.getValues();
var numRows = values.length;
var numCols = values[0].length;
for (var col = numCols - 1; col > 0; col--) {
for (var row = 0; row < numRows; row++) {
switch (values[row][col]) {
case "Group":
case "ID":
case "Reg ID":
case "Reg Date":
case "Type of Payment":
case "Transaction ID":
case "Coupon Code":
case "# Attendees":
case "Date Paid":
case "Price Option":
case "Event Date":
case "Event Time":
case "Website Check-in":
case "Tickets Scanned":
case "Check-in Date":
case "Seat Tag":
case "BLS Add-on items (received at class):":
case "Company Name":
case "Address":
case "Address 2":
case "City":
case "State":
case "Zip":
sheet.deleteColumn(col + 1); // delete column in sheet (1-based)
continue; // continue with next column
break; // can't get here, but good practice
}
}
}
}
//Remove Empty Columns
function RemoveEmptyColumns() {
var sh = SpreadsheetApp.getActiveSheet();
var maxColumns = sh.getMaxColumns();
var lastColumn = sh.getLastColumn();
sh.deleteColumns(lastColumn + 1, maxColumns - lastColumn);
}
//Rename Columns
function RenameColumns() {
SpreadsheetApp.getActiveSheet().getRange('A1').setValue('Type');
SpreadsheetApp.getActiveSheet().getRange('B1').setValue('Paid');
SpreadsheetApp.getActiveSheet().getRange('C1').setValue('Price');
SpreadsheetApp.getActiveSheet().getRange('D1').setValue('Amt');
SpreadsheetApp.getActiveSheet().getRange('E1').setValue('Class');
SpreadsheetApp.getActiveSheet().getRange('F1').setValue('First Name');
SpreadsheetApp.getActiveSheet().getRange('G1').setValue('Last Name');
SpreadsheetApp.getActiveSheet().getRange('H1').setValue('Email');
SpreadsheetApp.getActiveSheet().getRange('I1').setValue('Phone');
}
//Auto-Resize Columns
function ResizeColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
sheet.autoResizeColumn(1);
sheet.autoResizeColumn(2);
sheet.autoResizeColumn(3);
sheet.autoResizeColumn(4);
sheet.autoResizeColumn(5);
sheet.autoResizeColumn(6);
sheet.autoResizeColumn(7);
sheet.autoResizeColumn(8);
sheet.autoResizeColumn(9);
}
//Sort by last name
function Sort() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
sheet.sort(7);
}
//Save PDF
function SavePDF(optSSId, optSheetId) {
// If a sheet ID was provided, open that sheet, otherwise assume script is
// sheet-bound, and open the active spreadsheet.
var ss = (optSSId) ? SpreadsheetApp.openById(optSSId) : SpreadsheetApp.getActiveSpreadsheet();
// Get URL of spreadsheet, and remove the trailing 'edit'
var url = ss.getUrl().replace(/edit$/, '');
// Get folder containing spreadsheet, for later export
var parents = DriveApp.getFileById(ss.getId()).getParents();
if (parents.hasNext()) {
var folder = parents.next();
} else {
folder = DriveApp.getRootFolder();
}
// Get array of all sheets in spreadsheet
var sheets = ss.getSheets();
// Loop through all sheets, generating PDF files.
for (var i = 0; i < sheets.length; i++) {
var sheet = sheets[i];
// If provided a optSheetId, only save it.
if (optSheetId && optSheetId !== sheet.getSheetId()) continue;
//additional parameters for exporting the sheet as a pdf
var url_ext = 'export?exportFormat=pdf&format=pdf' //export as pdf
+
'&gid=' + sheet.getSheetId() //the sheet's Id
// following parameters are optional...
+
'&size=letter' // paper size
+
'&portrait=false' // orientation, false for landscape
+
'&fitw=true' // fit to width, false for actual size
+
'&sheetnames=false&printtitle=false&pagenumbers=false' // hide optional headers and footers
+
'&gridlines=true' // hide/show gridlines
+
'&fzr=false'; // do not repeat row headers (frozen rows) on each page
var options = {
headers: {
'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()
}
}
var response = UrlFetchApp.fetch(url + url_ext, options);
var blob = response.getBlob().setName(ss.getName() + ' - ' + sheet.getName() + '.pdf');
folder.createFile(blob);
}
}
/**
* Dummy function for API authorization only.
* From: https://stackoverflow.com/a/37172203/1677912
*/
function forAuth_() {
DriveApp.getFileById("Just for authorization"); // https://code.google.com/p/google-apps-script-issues/issues/detail?id=3579#c36
}

Great use for an add-on. In order to make it work as an add-on you need to create an onOpen() trigger so that the users can interact with your code.
Refer to the onOpen() docs here: https://developers.google.com/apps-script/guides/triggers/#onopen
See an example here: https://developers.google.com/apps-script/add-ons/#user_interfaces

Related

for loop on apps script running very slow, how can i make it run faster?

I have a code that I have been using for 2 years and it was working perfectly until recently. It started to give a timeout warning a couple of weeks ago and now it's not really running anymore.
The code pulls every picture from 13 different folders on google drive and then adds them to a sheet, it was giving the timeout warning after row 200. I modified it a bit but i'm still getting the exceeded maximum execution time warning (after 360 rows now)
I would really appreciate it if somebody had a solution for this. The script used to run pretty fast (around 2-3 rows per second) and now it's taking around 3-4secs per row.
Thank you so much in advance!
Here's the script:
function Iteration() {
var lista_carpetas = SpreadsheetApp.getActive().getSheetByName("carpetas").getRange("A2:A").getValues(); //event list
var lista_carpetas_ok = lista_carpetas.reduce(function(ar, e) {
if (e[0]) ar.push(e[0])
return ar;
}, []);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("data");
sheet.clear()
sheet.appendRow(["Name", "Date", "Size", "URL", "Download", "Description", "Image","Folder Id"]);
for (var i = 5; i <8; i++) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("data");
var folderId = lista_carpetas_ok[i];
var season1 = SpreadsheetApp.getActive().getSheetByName("carpetas").getRange("B2:B").getValues(); //event list
var season = season1.reduce(function(ar, e) {
if (e[0]) ar.push(e[0])
return ar;
}, []);
var folder = DriveApp.getFolderById(folderId);
var contents = folder.getFiles();
var cnt = 0;
var file;
while (contents.hasNext()) {
var file = contents.next();
cnt++;
Logger.log(file);
Logger.log(cnt);
// writes the various chunks to the spreadsheet- just delete anything you don't want
data = [
file.getName(),
file.getDateCreated(),
file.getSize(),
file.getUrl(),
"https://docs.google.com/uc?export=download&confirm=no_antivirus&id=" + file.getId(),
file.getDescription(),
"=image(\"https://docs.google.com/uc?export=download&id=" + file.getId() +"\")",
folderId,
];
sheet.appendRow(data);
};
}
}
From your explanations in your reply,
I didn't copy the whole code because the rest is just 4 more loops just like the one that i copied but for the other folders. So 4 loops in total pull the pictures from 13 different folders on google drive, and each loop pulls the pictures from 3-4 folders.
We have 13 folders on google drive which hold pictures from 2019-2022, and I made a loop that pulls the pictures from each folder that holds pictures for that year. Therefore the script has 4 for loops (only showing the first one above, which pulls the pictures from folders in rows 6 to 8, which are the folders for year 2018) that pull pictures from the folders on google drive.
I believe your current situation and your goal are as follows.
You have 13 folder IDs in the column "A" of "carpetas" sheet.
In your script, you want to use the values from "A6:A8" of "carpetas" sheet.
By retrieving the file metadata from the folders of folder IDs retrieved from "A7:A9" of "carpetas" sheet, you want to put the retrieved file metadata to "data" sheet.
You want to reduce the process cost of the script.
Modification points:
When you want to retrieve the folder IDs from the cells "A6:A8", when I saw your script, it seems that your script retrieves the values from the cells "A7:A9".
In your script, appendRow is used in the loop. In this case, the process cost becomes high. Ref
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("data") can be put out of the loop.
season is not used. So var season1 = SpreadsheetApp.getActive().getSheetByName("carpetas").getRange("B2:B").getValues() can be removed.
When these points are reflected in your script, it becomes as follows.
Modified script:
function Iteration() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var carpetasSheet = ss.getSheetByName("carpetas");
var lista_carpetas = carpetasSheet.getRange("A2:A" + carpetasSheet.getLastRow()).getValues();
var lista_carpetas_ok = lista_carpetas.filter(([a]) => a);
var sheet = ss.getSheetByName("data");
sheet.clear();
var files = [["Name", "Date", "Size", "URL", "Download", "Description", "Image", "Folder Id"]];
for (var i = 4; i < 7; i++) {
console.log(lista_carpetas_ok[i][0]) // Here, you can see the folder ID in the log.
var folderId = lista_carpetas_ok[i][0];
try {
var folder = DriveApp.getFolderById(folderId);
var contents = folder.getFiles();
while (contents.hasNext()) {
var file = contents.next();
var id = file.getId();
data = [
file.getName(),
file.getDateCreated(),
file.getSize(),
file.getUrl(),
"https://docs.google.com/uc?export=download&confirm=no_antivirus&id=" + id,
file.getDescription(),
"=image(\"https://docs.google.com/uc?export=download&id=" + id + "\")",
folderId,
];
files.push(data);
};
} catch (e) {
// In this modification, when your folder ID cannot be used, that folder ID is skipped. At that time, an error message can be seen in the log.
console.log(e.message);
}
}
if (files.length == 0) return;
sheet.getRange(1, 1, files.length, files[0].length).setValues(files);
}
Note:
This modified script retrieves the folder IDs from the cells "A6:A8" because of your reply of only showing the first one above, which pulls the pictures from folders in rows 6 to 8, which are the folders for year 2018. But if you want to retrieved the folder IDs from "A7:A9", please modify for (var i = 4; i < 7; i++) { to for (var i = 5; i < 8; i++) {.
References:
Benchmark: Reading and Writing Spreadsheet using Google Apps Script
setValues(values)
Try this:
function Iteration() {
const ss = SpreadsheetApp.getActive();
const sh0 = ss.getSheetByName("carpetas");
const evlist = sh0.getRange("A6:A9").getValues().flat();
const sh1 = ss.getSheetByName("data");
sh1.clear();
sh1.appendRow(["Name", "Date", "Size", "URL", "Download", "Description", "Image", "Folder Id"]);
for (let i = 0; i < 3; i++) {
let folder = DriveApp.getFolderById(evlist[i]);
let contents = folder.getFiles();
while (contents.hasNext()) {
let file = contents.next();
sh1.appendRow([file.getName(),file.getDateCreated(),file.getSize(),file.getUrl(),"https://docs.google.com/uc?export=download&confirm=no_antivirus&id=" + file.getId(),file.getDescription(),"=image(\"https://docs.google.com/uc?export=download&id=" + file.getId() + "\")",evlist[i]]);
};
}
}
If you must use ranges like "A2:A" then always use the form "A2:A" + sheet.getLastRow() otherwise you always have to remove the nulls inserted in the range from getLastRow() to getMaxRows() which is a big waste of time. In this case just using a fixed range actually works better since you're only using three rows.
You could be storing all of your image in Google Image Library and removing this with their API.

Can a GAS Library be used a script template?

I've built a system within a spreadsheet that uses custom menus to move through a process. The script is stored in a template file, which is generated from a separate overview spreadsheet (via script) when required. Basically, each newly generated spreadsheet is a Product Tracker.
When the template is copied for a new Product Tracker the script is out-of-date if the template is updated. Would extracting the bound script from the template and creating a library from it make sense for my application?
I've nearly finished developing the template for a particular product, but I would like to roll it out for multiple products with slightly different templates which means it would be difficult to have a generic script that could be published within my Google Workspace Domain as an add-on (would probably need an add-on for each product).
Reading up on Libraries, Google warns that they can be slow. Currently, each script takes around 10 seconds to run and performs operations such as hide/show sheets, change tab colours, add/remove protection, send emails etc. There are already calls to another library, would this affect it as well? Would this be really slow as a library?
Thanks in advance!
Edit:
There are 10 buttons on the menu, one for each stage. The API's below are used at every stage. Reading through the execution logs, the scripts vary in execution time from 4 - 6 seconds depending on the stage.
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/script.container.ui"
],
Edit2:
Bastardised the code to remove certain information.
The script below is similar across around 9 scripts.
function doThing() {
var activeUser = Session.getActiveUser().getEmail();
var activeUserName = activeUser.split('#domain.com')[0]
// spreadsheets
const ss = SpreadsheetApp.getActive();
// sheets
const 1 = ss.getSheetByName("1.");
const 1b= ss.getSheetByName("1b.");
const 2 = ss.getSheetByName("2.");
const 3 = ss.getSheetByName("3.");
const 4 = ss.getSheetByName("4.");
const 5 = ss.getSheetByName("5.");
const 6 = ss.getSheetByName("6.");
const 7 = ss.getSheetByName("7.");
const 8 = ss.getSheetByName("8");
const 9 = ss.getSheetByName("9.");
const shipSheet = ss.getSheetByName("Shipping");
const buildEvalSheet = ss.getSheetByName("Build Evaluation");
// build Number
const buildNumber = 1.getRange(buildNumberCell).getValue();
// get the active user email address and log it
Logger.log("Active User Email: " + activeUser);
Logger.log("Running 3");
// this prevents non-authorised users from running the script
// see the permissionGroups object in the Variables script file for more information
// set a variable relative to the department using the buttons
var criteria = 'department'
// run the permissions function in the library
var automationPermission = EmailPermissionsLibrary.checkPermissions(activeUserName, criteria);
// run if true
if (automationPermission) {
// set tab colours for previous & current
1.setTabColor("GREEN");
1b.setTabColor("GREEN");
2.setTabColor("GREEN");
3.setTabColor("GREEN");
// protect
3.activate()
protect();
statusUpdate()
if (emailBoolean) {
// send information to Sales and Shipping
//create html email
var htmlEmail = "<html><body>";
htmlEmail += "<p>Hello,</p>"
htmlEmail += "<p>" + buildNumber + " message</p>"
// send email
MailApp.sendEmail({
to: 'email#domain.com',
subject: buildNumber + " message",
htmlBody: htmlEmail,
replyTo: activeUser
});
}
shipping()
buildEvalShow()
}
// error message if the user is not authorised
else {
SpreadsheetApp.getUi().alert('This account does not have authorisation')
Logger.log(activeUser + " does not have required permissions")
}
}
Will this script be slow as a library?
// set a variable relative to the department using the buttons
var criteria = 'department'
// run the permissions function in the library
var automationPermission = EmailPermissionsLibrary.checkPermissions(activeUserName, criteria);
// log permission
Logger.log(automationPermission)
// run if true
if (automationPermission) {
Logger.log("Running");
// show sheet
sheet.activate();
// check that the #### hasn't already ran by doing a row count
// if it hasn't, the following should return 1 which will be the titles
var sheetR = sheet.getLastRow();
if (sheetR <= 1) {
// summary table size
const sumTableRowCount = 12;
const sumTableColCount = 3;
for (i = 1; i <= 3; i++) {
var results = eval("prodQC" + [i] + "Sheet");
sumTable = sheet2.getRange(45, 2, sumTableRowCount, sumTableColCount).getValues();
var something = sheet2.getLastRow() + 2;
// create the title
var title = "Title " + i;
// write the title
sheet.getRange(sheetR, 1).setValue(title);
// Remove checks
if (i >= 2) {
var sheetRL= sheet.getLastRow();
sheet.getRange(buildEvalSheetLastRow, 4).removeCheckboxes();
sheet.getRange(buildEvalSheetLastRow, 6).removeCheckboxes();
}
// recount last row and add 1 to stop overwrite
var sheetR = sheet.getLastRow() + 1;
// write the QC summary table to the sheet
sheet.getRange(sheetR, 1, sumTableRowCount, sumTableColCount).setValues(sumTable);
// set checkboxes next to values on JIRA col
sheet.getRange(sheetR, 4, sumTableRowCount, 1).insertCheckboxes();
// set checkboxes next to values on the CPACC col
sheet.getRange(sheetR, 6, sumTableRowCount, 1).insertCheckboxes();
// add data validation to the Choose One col
// last row
var sheetR = sheet.getLastRow();
// set range to be from row 3, col 8, to last row
var range = sheet.getRange(4, 8, sheetR, 1);
// set the data validation to only accept values from the array
range .setDataValidation(SpreadsheetApp.newDataValidation()
.setAllowInvalid(false)
.requireValueInList(
[
"list item 1",
"list item 2",
"listed item 3"
],
true)
.build()
);
// add data validation to the Impact and Deadline cols
// set range to be from row 3, col 11, to last row for 2 cols
var cols = sheet.getRange(4, 11, sheetR, 2);
// set the data validation to only accept values from the array
cols .setDataValidation(SpreadsheetApp.newDataValidation()
.setAllowInvalid(false)
.requireValueInList(
[
"1",
"2",
"3",
"4",
"5"
],
true)
.build()
);
}
// remove all non-failures
// last row
var sheetR = sheet.getLastRow();
// range of values from A1, across 3 columns and to the last row
var range = sheet.getRange(1, 1, sheetR, 3);
// Default text from Production QC Template
var removeVal1 = "something";
// Modified text when the QC is complete
var removeVal2 = "something else";
// Default text from Production QC Template
var removeVal3 = "another thing";
// get values from the range to check in the For loop below
var rangeVals = range.getValues();
// Reverse the 'for' loop.
for (let i = rangeVals.length - 1; i >= 0; i--) {
// if matching values above are in the target range, delete the row
if (
rangeVals[i][2] === removeVal1 ||
rangeVals[i][2] === removeVal2 ||
rangeVals[i][2] === removeVal3) {
buildEvalSheet.deleteRow(i + 1)
}
}
} else { // if there is already more rows than expected).alert('this has done something');
}
} else { // error message if the user is not authorised
SpreadsheetApp.getUi().alert('This account does not have authorisation')
Logger.log(activeUser + " does not have required permissions")
}
}

Can you create spreadsheet in gas & get ID & getActive?

Multiple versions of the spreadsheet with the subset of data exist on my drive and I have a way to get the most recent but I thought that if I could capture the ID when I created it I would not have to run that extra code. If I just create a spreadsheet the return is a 'spreadsheet' which I can .getActiveSheet().getRange(. . . ).setValues(. . .)
If I create and immediately getId then get use DriveApp.getFileById the return is a 'file'. It has a mime type of spreadsheet but cannot be used with .getActiveSheet() etc. Why? Is there a way to get the ID when creating a spreadsheet and still work with that new sheet?
function createSubset(fileName) {
//
// var mimeType = 'application/vnd.google-apps.spreadsheet';
baseSs = SpreadsheetApp.getActiveSpreadsheet(); // "StudentRosterForPractice"
baseSheet = baseSs.getSheetByName("Roster");
// there are formulas save below actual data but no in column B
var totDataRows = cntDataRows (baseSheet.getRange('B1:B').getValues()) - 1;
dataSubset = baseSheet.getRange(3, 2, totDataRows, 2).getValues();
// ---- this does NOT work ----
// tempSsId = SpreadsheetApp.create(fileName).getId();
// tempSs = DriveApp.getFileById(tempSsId); // returns file
// Logger.log("tempSs.getMimeType(): " + tempSs.getMimeType() );
//// tempSs.getMimeType(): application/vnd.google-apps.spreadsheet
// tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
//// gets TypeError: tempSs.getActiveSheet is not a function
// return tempSsId;
// ---- this does work ----
tempSs = SpreadsheetApp.create(fileName); // return spreadhsheet
tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
return null;
}
/**
* Number rows with actual data. There is a row of forumulas.
*/
function cntDataRows (colB) {
var count = 0 ;
colB.forEach(
function(item) {
if ( item != "" ) { // no formula in last name column
count = count + 1;
}
}
) ;
Logger.log("totDataRows: " + count);
return count
}
function testCreateSubset() {
var ssName = 'tempSs'; //subset name
tempSsId = createSubset(ssName);
}
Thank you Marios.
My subset spreadsheet only has one sheet so that was not a problem. I am surprised that I can getActiveSheet on the subset even though the script is bound to the base spreadsheet.
There is still a problem however. When I create my subset, return the ID and create the PDF I get these messages
totDataRows: 41
Nov 8, 2020, 8:54:51 AM Info spreadsheet ID: 1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM
Nov 8, 2020, 8:54:51 AM Info In makeBlobPdf spreadsheet ID: 1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM
Nov 8, 2020, 8:54:51 AM Info URL to be fetched: https://docs.google.com/spreadsheets/d/1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM/export?exportFormat=pdf&format=pdf&size=A4&portrait=false&fitw=true&sheetnames=false&printtitle=false&printnotes=false&pagenumbers=false&pagenum=CENTER&gridlines=false&fzr=true&top_margin=0.15&left_margin=0.15&right_margin=0.15&bottom_margin=0.15
but the PDF is empty. If I open the subset and get the ID, hardcode it in the script and create the PDF the messages are:
In makeBlobPdf spreadsheet ID: 1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM
Nov 8, 2020, 8:57:28 AM Info URL to be fetched: https://docs.google.com/spreadsheets/d/1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM/export?exportFormat=pdf&format=pdf&size=A4&portrait=false&fitw=true&sheetnames=false&printtitle=false&printnotes=false&pagenumbers=false&pagenum=CENTER&gridlines=false&fzr=true&top_margin=0.15&left_margin=0.15&right_margin=0.15&bottom_margin=0.15
The ID received in the make PDF code is the same and the URL generated is the same but the PDF where the ID was hardcoded has data and the PDF where it was passed from the create subset is empty. The create PDF code is unchanged.
Works:
var ssName = 'tempSs'; //subset name
// var spreadsheetId = createSubset(ssName);
// Logger.log("spreadsheet ID: " + spreadsheetId );
var spreadsheetId = "1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM";
var blobPdf = makeBlobPdf(spreadsheetId, ssName);
Returns empty PDF:
var ssName = 'tempSs'; //subset name
var spreadsheetId = createSubset(ssName);
Logger.log("spreadsheet ID: " + spreadsheetId );
// var spreadsheetId = "1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM";
var blobPdf = makeBlobPdf(spreadsheetId, ssName);
Corrected createSubset
function createSubset(fileName) {;
baseSs = SpreadsheetApp.getActiveSpreadsheet(); // "StudentRosterForPractice"
baseSheet = baseSs.getSheetByName("Sheet1");
// there are formulas saved below actual data but no in column B
var totDataRows = cntDataRows (baseSheet.getRange('B1:B').getValues()) - 1;
dataSubset = baseSheet.getRange(3, 2, totDataRows, 2).getValues();
tempSsId = SpreadsheetApp.create(fileName).getId();
tempSs = SpreadsheetApp.openById(tempSsId);
tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
return tempSsId;
}
Code that calls createSubset and makeBlobPdf
function createAndMailPdf () {
var ssName = 'tempSs'; //subset name
var spreadsheetId = createSubset(ssName);
Logger.log("spreadsheet ID: " + spreadsheetId );
// var spreadsheetId = "1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM";
var blobPdf = makeBlobPdf(spreadsheetId, ssName);
if (MailApp.getRemainingDailyQuota() > 0) {
var emailAddress = "blah blah # blah";
var subject = "With useless PDF file";
var htmlBody = HtmlService.createHtmlOutputFromFile('letterBody').getContent();
var templ = HtmlService.createTemplateFromFile('letterBody');
var message = templ.evaluate().getContent();
MailApp.sendEmail(
{ to: emailAddress,
subject: subject,
htmlBody: message,
attachments: blobPdf}
);
} else {
Logger.log("Mail quota exceeded but the PDF has been saved");
}
}
The makeBlobPdf code:
function makeBlobPdf (spreadsheetId, ssName) {
Logger.log("In makeBlobPdf spreadsheet ID: " + spreadsheetId );
var marginStringValue = '0.15';
var margin = '_margin=' + marginStringValue;
var margins = '&top' + margin + '&left' + margin
+ '&right' + margin + '&bottom' + margin;
var url = 'https://docs.google.com/spreadsheets/d/'
+ spreadsheetId + '/export?'
+ 'exportFormat=pdf&format=pdf'
+ '&size=A4'
+ '&portrait=false'
+ '&fitw=true' // Fit to width
+ '&sheetnames=false'
+ '&printtitle=false'
+ '&printnotes=false'
+ '&pagenumbers=false'
+ '&pagenum=CENTER'
+ '&gridlines=false'
+ '&fzr=true' // Repeat frozen rows
+ margins;
var token = ScriptApp.getOAuthToken();
Logger.log("URL to be fetched: " + url );
var result = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + token
}
});
var attachName = ssName + '.pdf';
var pdfBlob = result.getBlob().setName(attachName);
return pdfBlob;
};
Solution:
The issue in your code is that there is no method getActiveSheet()
for a file. It makes sense since a file can be anything in your
google drive. It wouldn't make sense to have a spreadsheet method like getActiveSheet() being applied to an image or a folder for example. As a result tempSs.getActiveSheet() is invalid since tempSs is a type of file in the deleted part of your code.
Here is the proper way to do that.
If you want to create the file and get its ID directly. And then you can use openById to get the spreadsheet.
function myFunction() {
var fileName = 'testname';
var file = Drive.Files.insert({title: fileName, mimeType: MimeType.GOOGLE_SHEETS}); // file
var newFile = SpreadsheetApp.openById(file.id); // Spreadsheet file
newFile.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
}
But keep in mind newFile.getActiveSheet() gives the first sheet in your file. You might want to use newFile.getSheetByName('Sheet1') instead.
To use this code please enable Drive API.
From the question
Can you create spreadsheet in gas & get ID & getActive?
Yes. Bear in mind that running getActiveSheet() for the created spreadsheet will return the firts sheet.
Regarding DriveApp.getFileById(id) it returns a File object not a Spreadsheet object.
The Class File hasn't a getActiveSheet() method. If you wan to use this method one opction is to use SpreadsheetApp.openById(id) or SpreadsheetApp.open(file)
Resources
https://developers.google.com/apps-script/reference/drive/drive-app
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app
The main problem you are encountering is that your sheet is being created but is taking some time to set the values in the function createSubset(). Therefore, while the creation of the Spreadsheet is complete, the values of the sheet have not been set yet which is done in the background.
This makes that createSubset() returns the Spreadsheet ID to the other functions and the PDF file is immediately created (but the cell values have not been set yet). This leads to your current behaviour where you are sending an empty Spreadsheets PDF because the cell values had not been set yet.
To force the cells to update with the right values and avoid sending the PDF before the Spreadsheet is all ready you can use the function flush which applies all the pending changes to the Spreadsheet. Use it as follows:
function createSubset(fileName) {;
baseSs = SpreadsheetApp.getActiveSpreadsheet(); // "StudentRosterForPractice"
baseSheet = baseSs.getSheetByName("Sheet1");
// there are formulas saved below actual data but no in column B
var totDataRows = cntDataRows (baseSheet.getRange('B1:B').getValues()) - 1;
dataSubset = baseSheet.getRange(3, 2, totDataRows, 2).getValues();
tempSsId = SpreadsheetApp.create(fileName).getId();
tempSs = SpreadsheetApp.openById(tempSsId);
tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
// FORCE ALL UPDATES IN THE SPREADSHEET BEFORE RETURNING THE SPREADHSEET ID
SpreadsheetApp.flush();
return tempSsId;
}

This script does not populate sheet after parsing retrieved data

I hope this is well explained. First of all, sorry because my coding background is zero and I am just trying to "fix" a previously written script.
Problem The script does not populate sheet after parsing retrieved data if the function is triggered by timer and the sheet is not open in my browser .
The script works OK if run it manually while sheet is open.
Problem details:
When I open the sheet the cells are stuck showing "Loading" and after a short time, data is written.
Expected behavior is to get the data written no matter if I don't open the sheet.
Additional info: This is how I manually run the function
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [
{name: "Manual Push Report", functionName: "runTool"}
];
sheet.addMenu("PageSpeed Menu", entries);
}
Additional info: I set the triggers with Google Apps Script GUI See the trigger
Before posting the script code, you can see how the cells look in the sheet:
Script code
function runTool() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Results");
var rows = activeSheet.getLastRow();
for(var i=3; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 2).getValue();
var stuff = "=runCheck"
if(workingCell != ""){
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
}
}
}
// URL check //
function runCheck(Url) {
var key = "XXXX Google PageSpeed API Key";
var strategy = "desktop"
var serviceUrl = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=" + Url + "&key=" + key + "&strategy=" + strategy +"";
var array = [];
var response = UrlFetchApp.fetch(serviceUrl);
if (response.getResponseCode() == 200) {
var content = JSON.parse(response.getContentText());
if ((content != null) && (content["lighthouseResult"] != null)) {
if (content["captchaResult"]) {
var score = content["lighthouseResult"]["categories"]["performance"]["score"];
} else {
var score = "An error occured";
}
}
array.push([score,"complete"]);
Utilities.sleep(1000);
return array;
}
}
You can try the code using the sheet below with a valid Pagespeed API key.
You only need to add a Trigger and wait for it's execution while the sheet is not open in your browser
https://docs.google.com/spreadsheets/d/1ED2u3bKpS0vaJdlCwsLOrZTp5U0_T8nZkmFHVluNvKY/copy
I suggest you to change your algorithm. Instead of using a custom function to call UrlFetchApp, do that call in the function called by a time-driven trigger.
You could keep your runCheck as is, just replace
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
by
activeSheet.getRange(i, 3, 1, 2).setValues(runCheck(url));
NOTE
Custom functions are calculated when the spreadsheet is opened and when its arguments changes while the spreadsheet is open.
Related
Cache custom function result between spreadsheet opens

Send email when cell contains a specific value

I am working on a Google Form that allows our employees to submit an in-field inspection of their equipment. I have a script that takes the form responses and creates a new sheet based on the date and the specific unit number of the equipment. The user goes through a checklist and selects either "Good" or "Needs Repair" for each item on the list. They can also add comments and upload pictures of any issues.
I am trying to have the script automatically send an email if "Needs Repair" is selected for any of the checks, as well as if the user adds a comment or a picture. This way we do not have to open every submitted sheet to know if any repairs are required. What I have is just not sending emails and I cannot figure out why. Any help is greatly appreciated!
Here is my current script:
function onFormSubmit() {
// onFormSubmit
// get submitted data and set variables
var ss = SpreadsheetApp.openById("*Spreadsheet Link*");
var sheet = ss.getSheetByName("Submissions");
var row = sheet.getLastRow();
var Col = sheet.getLastColumn();
var headings = sheet.getRange(1,1,1,Col).getValues();
var lastRow = sheet.getRange(row, 1, 1, Col);
var UnitNumber = sheet.getRange(row,3).getValue();
var newSheet = sheet.getRange(row,4,Col).getValue();
var fileExist = false;
var drillSheet = null;
var folder = DriveApp.getFoldersByName("Fraser Drill Inspections").next();
var files = folder.getFilesByName(UnitNumber);
var file = null;
var employee = sheet.getRange(row,2);
var checks = sheet.getRange(row, Col, 1, 20);
// check if Drill has sheet
while (files.hasNext())
{
fileExist = true;
file = files.next();
break;
}
if (fileExist) //If spreadsheet exists, insert new sheet
{
drillSheet = SpreadsheetApp.openById(file.getId());
drillSheet.insertSheet("" + newSheet);
}
else //create new spreadsheet if one doesn't exist
{
drillSheet = SpreadsheetApp.create(UnitNumber);
var ssID = drillSheet.getId();
file = DriveApp.getFileById(ssID);
file = file.makeCopy(UnitNumber, folder);
DriveApp.getFileById(ssID).setTrashed(true);
drillSheet = SpreadsheetApp.openById(file.getId());
drillSheet.renameActiveSheet(newSheet);
}
// copy submitted data to Drill sheet
drillSheet.getSheetByName(newSheet).getRange(1,1,1,Col).setValues(headings);
drillSheet.appendRow(lastRow.getValues()[0]);
drillSheet.appendRow(['=CONCATENATE(B6," ",B5)']);
drillSheet.appendRow(['=TRANSPOSE(B1:2)']);
//Hide top rows with raw data
var hiderange = drillSheet.getRange("A1:A3");
drillSheet.hideRow(hiderange);
//Widen columns
drillSheet.setColumnWidth(1,390);
drillSheet.setColumnWidth(2,700);
//Send email if there are any comments or if anything needs repair
if(lastRow.getValues() == "Needs Repair") {
function SendEmail() {
var ui = SpreadsheetApp.getUi();
MailApp.sendEmail("email#domain.com", "Drill Needs Repair", "This drill requires attention according to the most recent inspection report.")
}
}
}
The function to send an email is:
GmailApp.sendEmail(email, subject, body);
Try changing
if(lastRow.getValues() == "Needs Repair") {
function SendEmail() {
var ui = SpreadsheetApp.getUi();
MailApp.sendEmail("email#domain.com", "Drill Needs Repair", "This drill requires attention according to the most recent inspection report.")
}
}
to just the following:
if(lastRow.getValues() == "Needs Repair") {
GmailApp.sendEmail("youremail#domain.com", "Drill Needs Repair", "This drill requires attention according to the most recent inspection report.");
}
It looks like you've still got some additional work to do too, e.g. to make it send to the email address from the form submission instead of a hardcoded one.