Google Apps Script - How to execute functions one after another? - google-apps-script

1) My Goal
I'm trying to write a Google Apps Script that runs 3 functions step by step with a small pause after each step
Basically: 1) Fill cells with specific content 2) Send content from cells to email address 3) delete content from cells
2) My Challenge
Google Apps Script is running all functions simultanioulsy
3) What have I tried?
SpreadsheetApp.flush(); as suggested here - but did not work out
4) My Code
function pastecontent() {
// Fetch spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// copy paste from first row to all others
var source = sheet.getRange("D6:F6");
source.copyTo (sheet.getRange("D7:F22"));
}
function sendEmails() {
// pause for X seconds
Utilities.sleep(3000);
// Fetch spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// get the range and values in one step
var values = sheet.getRange("D7:F22").getValues();
// Send Mail
var message = values + " https://docs.google.com/spreadsheets/d/[....]";
var emailAddress = "XYZ#gmail.com";
var subject = "Test Mail";
if (cell != "") {
MailApp.sendEmail(emailAddress, subject, message);
}
}
function clearcontent() {
// pause for X seconds
Utilities.sleep(8000);
// Fetch spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// clear cells
sheet.getRange("D7:F22").clearContent();
}
Thanks for your support
Joe

Main function to call all functions:
function main() {
pastecontent();
sendEmails();
clearcontent();
}
Sample Log:
I haven't changed anything in your code aside from the email and it went smoothly.
Just make sure to have a main function that calls them one by one.
Aside from that, I'm not seeing any issues with your code
Nitpick: The cell variable wasn't defined before sending the email. Declaration wasn't included in the code provided so I declared it in mine. You might want to add that if you don't have it in yours.

I think you can organize your code like this
function mainFunction(){
pastecontent();
Utilities.sleep(200);// pause in the loop for 200 milliseconds
sendEmails();
Utilities.sleep(200);/
clearcontent();
}

Related

Google App Script to trigger on cell value change

I am a newbie and have been using a simple App Script to send out emails with triggers onEdit and onChange. However, my Worksheet has over ten sheets and any edits/changes (done by me or by computations) in any of the sheets sends out an email, causing unintended spam! To avoid this, if I could use some code that sends the email based only on ANY CHANGE to a specific cell's value, in a specific sheet, my problem would be solved. My outgoing email message is short and the whole message is in just ONE cell (C2). If I can add a line of code which monitors for ANY change in that cell C2, and sends out an email if there is a change, that's it! I'd be done. My Script is as follows:
function sendEmail(){
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet1=ss.getSheetByName('Email');
var emailAddress = sheet1.getRange(2,1).getValue();
var subject = sheet1.getRange(2,2).getValue();
var message = sheet1.getRange(2,3).getValue();
MailApp.sendEmail(emailAddress, subject, message);
}
Answer:
You can do this with an onEdit() and a conditional.
Code Example:
function onEdit(e) {
const specificSheet = "Email" // for example
const specificCell = "C2" // for example
let sheetCheck = (e.range.getSheet().getName() == specificSheet)
let cellCheck = (e.range.getA1Notation() == specificCell)
if (!(sheetCheck && cellCheck)) {
return
}
else {
sendEmail()
}
}
Rundown of this function:
Defines the sheet and A1 notation of the specific cell to check
Gets the Sheet and the A1 notation of the cell which was just edited
Returns if either the Sheet or the Cell are not the defined specific cell (using De Morgan's law)
Runs sendEmail() if the cell and Sheet are correct
References:
Event Objects | Apps Script | Google Developers
Simple Triggers | Apps Script | Google Developers
De Morgan's laws - Wikipedia
Working from the answer, this is how it ended up for me since I had several ranges to check:
function onEdit(e){
if(wasEdited(e, range1)){ // e.g. range1 = "Sheet1!A5"
// handle range1 change
}
if(wasEdited(e, range2)){ // e.g. range2 = "Sheet1!A7"
// handle range2 change
}
}
function wasEdited(e, range){
let tab = getTabFromA1Range(range)
let cell = getRangeFromA1Range(range)
return e.range.getSheet().getName() == tab && e.range.getA1Notation() == cell
}
function getTabFromA1Range(a1Range){
return a1Range.substring(0, a1Range.indexOf("!"))
}
function getRangeFromA1Range(a1Range){
return a1Range.substring(a1Range.indexOf("!")+1)
}
On further research, the following solution seems to work the best:
function sendEmail(){
Utilities.sleep(30000);
var ss=SpreadsheetApp.getActiveSpreadsheet();
var data=ss.getActiveSheet().getActiveCell().getA1Notation();
var sheetname = ss.getActiveSheet().getName();
var sheet1=ss.getSheetByName('Email');
var emailAddress = sheet1.getRange(2,1).getValue();
var subject = sheet1.getRange(2,2).getValue();
var message = sheet1.getRange(2,3).getValue();
if(data.indexOf('A:C')!=-1.23456789) {
MailApp.sendEmail(emailAddress, subject, message);
}
};
The key seems to be the "if statement" on line 10. Please note the time delay of half a minute I added to the script. This is because without it, on the trigger activating, the previous email was going out instead of the current one. Obviously my app has a slight delay in syncing and the trigger fired before all the current data got populated in the relevant cell!

Adding a number to an event in a Sheet once a Form has submitted data to the Sheet

I am trying to create a script to add an "Incident" number to the Incident Number column (Column 1) when a form is submitted and the Data is added to the linked Google Sheet (Sheet Name "IIR") by detecting new data in Column 4. Row one contains the Sheet Headers (developed by the questions in the Form). I pilfered a script with the same intent from other sources, the Script runs with no errors but I am not getting the desired result to the sheet (I am a Newbee).
//CORE VARIABLES
// The column you want to check if something is entered.
enter code here var COLUMNTOCHECK = 4; // Where you want the date time stamp offset from the input location. [row, column]
var ADDNUMBER = [1,1]; // Sheet you are working on
var SHEETNAME = "IIR"
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("IIR"); //checks that we're on the correct sheet.
if( sheet.getSheetName() == SHEETNAME ) {
var selectedCell = ss.getActiveCell(); //checks the column to ensure it is on the one we want to cause the number to appear.
if( selectedCell.getColumn() == COLUMNTOCHECK) {
var IncidentNumber = selectedCell.offset(ADDNUMBER[1],ADDNUMBER[1]);
for (r=1; r<999; r++) {
ss.getRange(r,1).setValue(r);
}
}
}}
Append incident number to time stamp in linked sheet
This function will add a incident number that is made up from the time stamp by using a two digit year followed by a two digit month followed by a two digit day and then a dash and then consecutive incrementing numbers. It also keeps the timestamp and adds the incident number to the bottom of the cell.
function formSubmit(e) {
if(e.values && e.values[1]) {
Logger.log(JSON.stringify(e));
var sh=e.range.getSheet();
sh.getRange(e.range.rowStart,1).setValue(getIncidentNumber(e));
}
}
function getIncidentNumber(e) {
var ps=PropertiesService.getScriptProperties();
var ts=e.values[0];
var key=Utilities.formatString('%s%s%s',ts.slice(8,10),ts.slice(0,2),ts.slice(3,5));
if(ps.keys && ps.getKeys().indexOf(key)==-1) {
ps.setProperty(key,1);
return key + "-" + 1;
}else{
var value=ps.getProperty(key);
ps.setProperty(key, Number(value) + 1)
var rv = ts + '\n' + key + "-" + Number(value).toFixed();
return rv;
}
}
If you want to get the Incident number from the sheet you should open the Script editor from the Form. In this way, the script will be bound to the actual form.
Afterwards, you should be using the following script:
var DESTINATION_SS_ID = "YOUR_DESTINATION_SHEET_ID";
function onFormSubmit(e) {
var incidentNumber = e.response.getItemResponses()[2].getResponse();
var ss = SpreadsheetApp.openById(DESTINATION_SS_ID);
ss.appendRow([incidentNumber]);
}
The script is using an installable trigger and instead of going through the bounded sheet from the form, like you did, it takes the response directly from the form and adds it to your wanted spreadsheet. This is done by using the onFormSubmit installable trigger.
To install the trigger you should follow these steps:
Go to your project's triggers by clicking this icon:
Create a new trigger with the following settings:
Save the project.
For the trigger to work, you/other users just have to send responses through the form. The results are obtained by using the .getItemResponses() method followed by the .getResponse() method, which essentially get you the responses you/other users have sent. Afterwards, these responses are added to the sheet by using the .appendRow() method; so every time a new entry gets submitted in the form, the Incident number is automatically added to your spreadsheet.
Furthermore, I suggest you take a look at these links since they might be of help in your future development:
Installable Triggers;
Event objects - Form submit;
FormResponse Class;
ItemResponse Class;
Sheet Class - appendRow().

Stand Alone SpreadsheetApp.getActiveSheet() always return the first sheet

SpreadsheetApp.getActiveSheet() will always return the first sheet rather than the active sheet which is most likely a bug in new sheets.
function testActiveSheet(){
var ssCentral = SpreadsheetApp.openById("xxxxxxxxxxx");
var ssCentralName = ssCentral.getName();
SpreadsheetApp.setActiveSpreadsheet(ssCentral);
Logger.log('the ssCentralName is ' + ssCentralName);
var currentActiveSheet = ssCentral.getActiveSheet();
var currentActiveSheetName = currentActiveSheet.getName();
Logger.log('the currentActiveSheetName is ' + currentActiveSheetName);
}
despite the active sheet is not the first one, it still returns the first one.
If the script is stand-alone and not bound to a specific spreadsheet, an active Sheet would not exist. This is not a bug but expected behaviour. To expand an active sheet is what a user is looking at, not what the script is currently working in; or as said in the official documentation, "the active sheet in a spreadsheet is the sheet that is being displayed in the spreadsheet UI."
If the script is not bound then there is no sheet being displayed to any user and therefore the function just returns the first sheet! Hope that makes sense, let me know if you have any follow-up questions.
See here for details.
in response to #Chris
this is the SCRIPT B
function callActivateNewMember(){
LibraryName.activateNewMember()
}
this is the standalone SCRIPT A
function activateNewMember(){
// DECLARE THE CURRENT ACTIVE WORKSHEET as ssCentral
var ssCentral = SpreadsheetApp.openById("xxxxxxxxxxxxxx");
// check if TeamList sheet is active - if not land on Teamlist and ask to select a
new member by his/her first name
// goto and activate TeamList
var teamListSheet = ssCentral.getSheetByName('TEAM LIST');
var teamListSheetIndex = teamListSheet.getIndex()-1;
Logger.log('the teamListSheetIndex is ' + teamListSheetIndex);
var currentSheet = SpreadsheetApp.getActiveSheet().getName();
Logger.log('the currentSheet is ' + currentSheet);
if(currentSheet !='TEAM LIST'){
ssCentral.setActiveSheet(ssCentral.getSheets(
[teamListSheetIndex]).getRange('B5').activate();
Logger.log('WRONG TABLE');
return;
}
var teamListSheetLastRow = teamListSheet.getLastRow();
var nberRows = teamListSheetLastRow-7
// CHECK IF THE CELL IS IN THE FIRST COL AND IS NOT EMPTY
// ------------------get the current row of the member selected
var MemberRow = ssCentral.getCurrentCell().getRow();
var MemberCol = ssCentral.getCurrentCell().getColumn();
if(MemberRow <8 ||MemberCol>1 ){
Logger.log('RIGHT TABLE - WRONG CELL SELECTED');
return;
}
var memberCellValue = ssCentral.getCurrentCell().getValue();
if(memberCellValue=='' ){
Logger.log('CELL SELECTED EMPTY');
return;
}
// VALIDATION : IS THIS MEMBER WAS ALREADY ACTIVATED
//-----------get the column with “Reporting Activated” as header
var repActiveCol = teamListSheet.getRange("D7").getColumn();
//-----------check if the cell of “Reporting Activated” is Y
var checkY = teamListSheet.getRange(MemberRow, repActiveCol).getValue();
//-----------If checkY = “Y” then Alert OK
if (checkY == "Y") {
Logger.log('MEMBER WAS ALREADY ACTIVATED');
return;
}
// rest of the function
}
after considering the answers from Chris I decided to change the strategy
I now call the function in the standalone SCRIPT along with 2 parameters I catch from the SCRIPT bound to the Spreadsheet: ActiveSheet + ActiveCell.
then I return an error message based on the situation and display alert based on the situation.
it works now but I am not totally satisfied as I wanted to show the most limited code in the SCRIPT bound to the Spreadsheet.
thanks again to Chris for taking the time to explain to me the issue I faced. if you pass by Bali - Indonesia just let me know I'd be glad to have a coffee with you. We have a dev center here.
If your gs script was created from within a Google Sheet you can use
var sheet = SpreadsheetApp.getActiveSheet();
to get the currently selected sheet

Pass a variable between a script bound to a Google Form and a script bound to its destination sheet

I am having issues getting information from a Google Form into a Google Sheet. I am looking to get the edit url onFormSubmit and then set it to the end of the record in a column where the responses are stored.
Research:
I asked this question, which started as a script bound to the sheet but trying to access the form. It then became a script bound to the form, trying to access the sheet.
I found this question which looks to be related to my question (with a slightly different use case). Similarly to mine, I think it will have issues getting spreadsheet methods while on the form.
Since both required methods that are only available to either the script or the form I keep hitting a wall. Now I am thinking that I may need a hybrid solution that requires some of the code to be bound to the sheet, and some to be bound to the form, with a variable passed between the two scripts that are both executing onFormSubmit.
This is what I think I should keep bound to the form
function onFormSubmit(e)
{
Logger.clear; //if I can use log to pass variable I want to clear out at the beginning of each submission
var form = FormApp.getActiveForm();
var activeFormUrl = form.getEditUrl();//This is the variable I need to pass to the sheet
Logger.log(activeFormUrl); //only to confirm what we are getting unless I can somehow access the log after the fact using sheet script
}//This is the end of onFormSubmit function bound to the Form
This is what I think I should keep bound to the sheet
function onFormSubmit(e)
{
var ss = SpreadsheetApp.getActiveSheet();
var createDateColumn = ss.getMaxColumns(); //CreateDateColumn is currently in AX (Column 50) which is the last/max column position
var urlColumn = createDateColumn-1; //urlColumn is currently in AX (Column 50) Calculating using it's relative position to createDateColumn Position
if (ss.getActiveRange(urlColumn).getValue() == "") // so that subsequent edits to Google Form don't overwrite editResponseURL
{
var editResponseURL = setGoogleFormEditUrl(ss, createDateColumn, activeFormUrl);
var createEditResponseUrl = ss.getActiveRange(urlColumn);
createEditResponseUrl.setValue(activeFormUrl);
}
else
{
if (ss.getActiveRange(urlColumn).getValue() != activeFormUrl)
{
Logger.log("Something went wrong - URL doesn't match" + activeFormUrl);
Logger.log(ss.getActiveRange(urlColumn).getValue());
var checkLog2 = Logger.getLog();
}
else {}//do nothing
}
}//This is the end of the onFormSubmit function bound to the Sheet
What I need to know is how to take activeFormUrl from the form script and send it to the sheet script. Can I use the log?
I'm not sure if this would work for you, but you can make an HTTPS GET or POST request to an Apps Script project with UrlFetchApp.fetch(url). So, from the Form project, you can make an HTTPS POST request to a published Web App. The published Web App can actually be published from the project bound to the spreadsheet, if you want to do that.
The way that an Apps Script project detects an HTTPS GET or POST request being sent to it, is with either a doGet() or doPost() function.
var webAppUrl = "https://script.google.com/macros/s/123_My_FileID/exec";
var payload = {
"url":"activeFormUrl"
};
var options = {"method":"post","payload":payload};
UrlFetchApp.fetch(webAppUrl, options);
The above code makes a POST request to another Apps Script project, and sends the payload to the file.
function doPost(e) {
var theUrl = e.parameter.url;
};
I'm assuming that you are trying to have a spreadsheet that is getting data from multiple Forms?
I had to separate the form and the spreadsheet operations as getting the formEditURL using the FormApp method would not work if I was using other SpreadsheetApp methods in the same function and the FormApp method only worked if it was in the onFormSubmit function.
Here is the code snippet which I used successfully
function onFormSubmit(e)
{
var rng = e.range; //Collects active range for event
var ss = SpreadsheetApp.getActiveSpreadsheet();//collects active spreadsheet object
var fUrl = ss.getFormUrl();//gets form url linked with active spreadsheet
var f = FormApp.openByUrl(fUrl);//opens form using form url
var rs = f.getResponses(); //gets responses from active form
var r = rs[rs.length - 1]; // Get last response made on active form
var c = getCellRngByCol(rng, 'formEditURL'); //locates the cell which the form url will be stored by searching header name
c.setValue(r.getEditResponseUrl());// sets form url value into correct cell for active form response
var callSpreadsheetFunctions = spreadsheetFunctions(rng, ss); //method calls other spreadsheet functions. This had to be modularized as you can't get form url if the other functions are occuring in the same function
}//This is the end of the onFormSubmit function
function spreadsheetFunctions (rng, ss)
{
var rowIndex = rng.getRowIndex();//gets row index for current response. This is used by tracking number
var createDateCell = getCellRngByCol(rng, 'CreateDate'); //locates which cell the createdate will be stored in by searching header name
var timestampCell = getCellRngByCol(rng, 'Timestamp'); //locates which cell the autogenerated timestamp is located in by searching header name
var trackingNumberCell = getCellRngByCol(rng, 'Tracking ID#');//locates which cell the tracking ID# will be stored in by searching by header name
var createDate = setCreateDate(rng, createDateCell, timestampCell); //method sets create date. NOTE: Function not included in code snippet but left here to demonstrate type of information used
var trackingNumber = setTrackingNumber(rng, rowIndex, trackingNumberCell, createDateCell); //method sets tracking number. NOTE: Function not included in code snippet but left here to demonstrate type of information used
return;
} //This is the end of the callSpreadsheetFunctions function
function getCellRngByCol(rng, col)//finds the cell associated with the active range and column
{
var aRng = SpreadsheetApp.getActiveSheet().getDataRange();//gets the spreadsheet data range
var hRng = aRng.offset(0, 0, 1, aRng.getNumColumns()).getValues();//finds the header row range by offsetting
var colIndex = hRng[0].indexOf(col);// declares the column index in the header row
return SpreadsheetApp.getActiveSheet().getRange(rng.getRow(), colIndex + 1); //returns the cell range at the position of the active row and column name passed into this method
}//This is the end of the getCellRngByCol function

Google Script: Issue deleting Specific Sheet in google spreadsheet

Situation:
I have a spreadsheet with 20 sheet.
I have other script that copy sheets from other spreadsheet every days to this spreadsheet-
I need to delete every days some specific sheet from a particular spreadsheet.
Problem:
When the script ends to clear the sheets, the spreadsheet hangs and I have to exit and re-enter to the spreadsheet.
I'll appreciate if anyone can help to tunning this script to work without hangging the spreadsheet.
Script:
function shellDeleteSheets(){
var sheets = ['Sheet1','Sheet2','Sheet3','Sheet4','Sheet5'];
for (var s in sheets){
deleteSheetName(sheets[s]);
}
}
function deleteSheetName(stname) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName(stname);
if(!sh) {
return;
}
ss.setActiveSheet(sh);
ss.deleteActiveSheet();
Utilities.sleep(400);
SpreadsheetApp.flush();
}
Try this version I use without issue
function DeleteSheets(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ['sheet1','sheet2','sheet3'];
var numberOfSheets = ss.getSheets().length;
for(var s = numberOfSheets-1; s>0 ; s--){ // in my case I never delete the first sheet
SpreadsheetApp.setActiveSheet(ss.getSheets()[s]);
var shName = SpreadsheetApp.getActiveSheet().getName();
if(sheets.indexOf(shName)>-1){
var delSheet = ss.deleteActiveSheet();
Utilities.sleep(500);
}
}
SpreadsheetApp.setActiveSheet(ss.getSheets()[0]);// send me back to first sheet
}
You could of course use the array of names as an argument for the function or - that's what I do in some cases - give the names of the sheet I need to keep, in this case the if condition is just different .
the sleep can be 400 mS, not sure it makes any difference, I use 500 because at some time I found it more reliable... and I (try to) never change a working solution ;-)
EDIT following your comment :
to make active the sheet called 'Updated' just change the last line of the code like this :
SpreadsheetApp.setActiveSheet(ss.getSheetByName('Updated'));
please note also that I moved this line in the original code, outside of the loop, sorry for this small error ^^