This is a follow up to my last question How to loop an onEdit function to send emails from multiple rows in Google Sheets? Now to finish this project, I need to be able to send SMS based on a cell's contents.
I'm using Twilio, and the code from their example https://www.twilio.com/blog/2016/02/send-sms-from-a-google-spreadsheet.html allows me to send texts to ALL numbers in the spreadsheet when I run the function. There is some help at this question Send SMS from Google Sheet however since I'm using Twilio instead of carrier emails I'm still getting stuck.
As of right now, this first block of code allows all texts to be send at the same time I run the function (sendSMS contains all API info and is not shown):
function sendAll() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('HIVE');
var startRow = 2;
var width = 16;
var numRows = sheet.getLastRow() - 1;
var dataRange = sheet.getRange(startRow, 2, numRows, width)
var data = dataRange.getValues();
for (i in data) {
var row = data[i];
try {
response_data = sendSms(row[4], row[12]);
status = "sent";
} catch(err) {
Logger.log(err);
status = "error- Not Sent";
}
sheet.getRange(startRow + Number(i), 2).setValue(status);
}
}
I tried replicating the pattern used to send the emails in my last question by inserting:
if (sheet.getSheetName() == sheetname && range.columnStart == 1) {
var data = sheet.getRange(range.getRow(), 1, 1, 21).getValues()[0];
var object = {
to: data[5] // Column "E"
if (e.value == "Appt. Set (send text)") {
object.subject = "Appt. Confirmation";
object.body = apptText; // variable containing body text
if (object.subject) sendSms(object);
I've updated the code to where it will trigger onEdit, but only for the first IF statement:
function onEditText(e) {
var sheetname = "HIVE";
var sheet = e.range.getSheet();
var range = e.range;
var timezone = "GMT-5";
var timestamp_format = "MMMM dd 'at' HH:mm";
var date = Utilities.formatDate(new Date(), timezone, timestamp_format);
var twilioNumber = 1234567890;
if (sheet.getSheetName() == sheetname && range.columnStart == 1) {
var data = sheet.getRange(range.getRow(), 1, 1, 21).getValues()[0];
var object = {
to: data[5] // Column "E"
};
var apptText = "Hey " + data[9] + "! 😊\n\nThanks...;
var leadText = "Hey " + data[9] + "! 👋\n\nThanks so...;
var followText = "Hey " + data[9] + "! 😊 \n\nAre...;
var confirmText = "Hey " + data[9] + "! ⏰ \n\nYour appointment...;
if (e.value == "Appt. Set (send text)") {
object.subject = "Appt. Confirmation";
object.body = apptText; //
SpreadsheetApp.getActiveSheet().getRange(range.getRow(),2,1,1).setValue('Appt. Set Text sent on ' + date);
} else if (e.value == "Lead (send 1st text)") {
object.subject = "Lead";
object.body = leadText; //
SpreadsheetApp.getActiveSheet().getRange(range.getRow(),2,1,1).setValue('Lead 1st Text sent on ' + date);
} else if (e.value == "3rd Text") {
object.subject = "Follow Up";
object.body = followText;
SpreadsheetApp.getActiveSheet().getRange(range.getRow(),2,1,1).setValue('3rd Text sent on ' + date);
}else if (e.value == "Day of Confirm (send text)") {
object.subject = "Can't wait to meet you!";
object.body = confirmText;
SpreadsheetApp.getActiveSheet().getRange(range.getRow(),2,1,1).setValue('Confirmation Text sent on ' + date);
}
if (object.subject) sendSms(data[5], twilioNumber);
}
}
Related
I'm writing a Google Sheets add-on that copies some data from one spreadsheet to another and then reformats it. The data sets involved are often large (~100k rows), so to avoid hitting the 6-minute timeout limit I break the data into chunks and then run the data-copying function in parallel on each chunk using google.script.run calls from the client side.
On my sample data set of ~100k rows, the first couple of chunks to complete are copied successfully, and the rest are throwing the error "Service Spreadsheets timed out while accessing document with id [spreadsheet id]."
And here's what it looks like in the Apps Script dashboard:
I'm confused by the timeout errors because:
I've successfully run the script on a dataset that contains 5000 rows
Apps Script dashboard shows the executions failing before 6 minutes (more like 4-5 minutes)
Apps Script dashboard logging shows the failed (timed out) executions logging successfully. Logging happens after the setValues() operation (see code below); the only thing that comes after the logging is the return, so I don't understand how it could log successfully and then time out (I thought Apps Script was synchronous... but maybe I'm wrong?)
I'm also not sure about those "Uncaught" errors, but they seem to be showing up on the dashboard as "Document [spreadsheet id] is missing (perhaps it was deleted, or you don't have read access?)"
This is the document I'm copying to, and I've confirmed that it still exists on my Drive and I can open it and see the data that was successfully copied. Can a document go "missing" if too many instances of a script are trying to access it simultaneously?
I've experimented with smaller chunk sizes (1000 and 2000 rows) and get the same types of errors.
Here's what my client-side Javascript looks like:
// This function is the success handler that runs after another function (which grabs the total # of rows
// from the sheet to be copied, and then creates the new spreadsheet to be copied into) completes
function dataParamsSuccess(dataParameters) {
// dataParameters = [busHrs, outputSsUrl, lastRow, maxRows, maxColumns]
var busHrs = dataParameters[0];
var outputSsUrl = dataParameters[1];
var lastRow = dataParameters[2];
var maxRows = dataParameters[3];
var maxColumns = dataParameters[4];
console.log(maxRows);
console.log(maxColumns);
// Set chunk size
var chunkSize = 5000; // number of rows in chunk
// Determine number of chunks
var numChunks = Math.ceil(lastRow / chunkSize);
var lastChunkSize = lastRow % chunkSize;
if ((numChunks-1) * chunkSize + lastChunkSize == lastRow) {
console.log("Math checks out");
} else {
console.log("oops, check your math");
}
// Generate status message
var statusHtml = numChunks + " chunks to be copied";
for (i=0; i<numChunks; i++) {
var chunkNum = i+1;
var chunkNumStr = chunkNum.toString();
statusHtml += "<div id=\"chunk" + chunkNumStr + "Status\"></div>";
}
document.getElementById("statusMsg").innerHTML = statusHtml;
var startRow = 1;
// Call copyData once for each chunk
for (i=0; i<numChunks; i++) {
var chunkNum = i+1;
var chunkNumStr = chunkNum.toString();
var chunkDivId = "chunk" + chunkNumStr + "Status";
if (chunkNum==numChunks) { // if this is the last chunk, chunk size is smaller
chunkSize = lastChunkSize;
}
var copyParams = [chunkNum, chunkSize, startRow, outputSsUrl];
google.script.run
.withSuccessHandler(copyChunkSuccess)
.copyData(copyParams);
document.getElementById(chunkDivId).innerHTML = "Chunk " + chunkNumStr + " copying in progress";
startRow += chunkSize;
console.log("startRow: " + startRow.toString());
}
// Haven't gotten to the part where I figure out what to do after all chunks are complete yet
}
And here's the server-side Apps Script function being called:
function copyData(copyParams) {
try {
// copyParams = [chunkNum, chunkSize, startRow, outputSsUrl]
var chunkNum = copyParams[0];
var chunkSize = copyParams[1];
var startRow = copyParams[2];
var outputSsUrl = copyParams[3];
var lastRow = startRow + chunkSize;
// Get input and output sheets
var dataSheet = SpreadsheetApp.getActiveSheet();
var outputSpreadsheet = SpreadsheetApp.openByUrl(outputSsUrl);
var outputSheet = outputSpreadsheet.getActiveSheet();
// Copy values
var values = dataSheet.getRange(startRow, 1, chunkSize, 22).getValues();
outputSheet.getRange(startRow, 1, chunkSize, 22).setValues(values);
// Logging
var dataSpreadsheetId = dataSheet.getParent().getId();
var outputSpreadsheetId = outputSpreadsheet.getId();
console.log("Chunk " + chunkNum.toString() + " (rows " + startRow.toString() + " through " + lastRow.toString() + ") copied successfully");
return [chunkNum, startRow, lastRow, "success"];
} catch(e) {
return [chunkNum, startRow, lastRow, e.message]; // Return error to client-side; server-side logging is taking too long
}
}
How about this answer?
In my experience, even when the Spreadsheet service is used, when the continuous accesses occurs with the asynchronous process, I have experienced such issue. At that time, I used the lock service and setTimeout. But I'm not sure whether this method can resolve your issue. So please test the following modification. Here, I would like to propose to use the lock service for Google Apps Script side and setTimeout for Javascript side. When your script is modified, it becomes as follows.
The flow of this workaround is as follows.
Flow:
10 workers are sent to Google Apps Script side.
After 10 workers were sent, it waits for 5 seconds.
At Google Apps Script side, 10 workers are received. And these are processed under the lock service.
After 5 seconds, at Javascript side, next 10 workers are sent.
By this cycle, the script is run.
Google Apps Script side:
Please modify copyData as follows.
function copyData(copyParams) {
var lock = LockService.getDocumentLock();
if (lock.tryLock(10000)) {
try {
// copyParams = [chunkNum, chunkSize, startRow, outputSsUrl]
var chunkNum = copyParams[0];
var chunkSize = copyParams[1];
var startRow = copyParams[2];
var outputSsUrl = copyParams[3];
var lastRow = startRow + chunkSize;
// Get input and output sheets
var dataSheet = SpreadsheetApp.getActiveSheet();
var outputSpreadsheet = SpreadsheetApp.openByUrl(outputSsUrl);
var outputSheet = outputSpreadsheet.getActiveSheet();
// Copy values
var values = dataSheet.getRange(startRow, 1, chunkSize, 22).getValues();
outputSheet.getRange(startRow, 1, chunkSize, 22).setValues(values);
// Logging
var dataSpreadsheetId = dataSheet.getParent().getId();
var outputSpreadsheetId = outputSpreadsheet.getId();
console.log("Chunk " + chunkNum.toString() + " (rows " + startRow.toString() + " through " + lastRow.toString() + ") copied successfully");
return [chunkNum, startRow, lastRow, "success"];
} catch(e) {
return [chunkNum, startRow, lastRow, e.message]; // Return error to client-side; server-side logging is taking too long
} finally {
lock.releaseLock();
}
}
}
HTML & Javascript side:
Please modify dataParamsSuccess as follows.
// This function is the success handler that runs after another function (which grabs the total # of rows
// from the sheet to be copied, and then creates the new spreadsheet to be copied into) completes
async function dataParamsSuccess(dataParameters) { // <--- Modified
const wait = (s) => new Promise(r => setTimeout(r, s)); // <--- Added
// dataParameters = [busHrs, outputSsUrl, lastRow, maxRows, maxColumns]
var busHrs = dataParameters[0];
var outputSsUrl = dataParameters[1];
var lastRow = dataParameters[2];
var maxRows = dataParameters[3];
var maxColumns = dataParameters[4];
console.log(maxRows);
console.log(maxColumns);
// Set chunk size
var chunkSize = 5000; // number of rows in chunk
// Determine number of chunks
var numChunks = Math.ceil(lastRow / chunkSize);
var lastChunkSize = lastRow % chunkSize;
if ((numChunks - 1) * chunkSize + lastChunkSize == lastRow) {
console.log("Math checks out");
} else {
console.log("oops, check your math");
}
// Generate status message
var statusHtml = numChunks + " chunks to be copied";
for (i = 0; i < numChunks; i++) {
var chunkNum = i + 1;
var chunkNumStr = chunkNum.toString();
statusHtml += "<div id=\"chunk" + chunkNumStr + "Status\"></div>";
}
document.getElementById("statusMsg").innerHTML = statusHtml;
var count = 0; // <--- Added
var startRow = 1;
// Call copyData once for each chunk
for (i = 0; i < numChunks; i++) {
count++; // <--- Added
var chunkNum = i + 1;
var chunkNumStr = chunkNum.toString();
var chunkDivId = "chunk" + chunkNumStr + "Status";
if (chunkNum == numChunks) { // if this is the last chunk, chunk size is smaller
chunkSize = lastChunkSize;
}
var copyParams = [chunkNum, chunkSize, startRow, outputSsUrl];
google.script.run
.withSuccessHandler(copyChunkSuccess)
.copyData(copyParams);
if (count == 10) { // <--- Added
console.log("wait");
await wait(5000);
count = 0;
}
document.getElementById(chunkDivId).innerHTML = "Chunk " + chunkNumStr + " copying in progress";
startRow += chunkSize;
console.log("startRow: " + startRow.toString());
}
// Haven't gotten to the part where I figure out what to do after all chunks are complete yet
}
Note:
I'm not sure whether 5000 of await wait(5000) is suitable for your situation. So please modify this value by testing at your situation. In the current value, 5000 is 5 seconds.
Reference:
Lock Service
Here is the GS code which will fetch the email id and and subject and the table.
var EMAIL_DRAFTED = "EMAIL DRAFTED";
function draftMyEmails() {
var sheet = SpreadsheetApp.getActiveSheet(); // Use data from the active sheet
var startRow = 2; // First row of data to process
var numRows = sheet.getLastRow() - 1; // Number of rows to process
var lastColumn = sheet.getLastColumn(); // Last column
var dataRange = sheet.getRange(startRow, 1, numRows, lastColumn) // Fetch the data range of the active sheet
var data = dataRange.getValues(); // Fetch values for each row in the range
// Work through each row in the spreadsheet
for (var i = 0; i < data.length; ++i) {
var row = data[i];
// Assign each row a variable
var clientName = row[0]; // Col A: Client name
var clientEmail = row[1]; // Col B: Client email
var sub = row[2]; // Col C: subject
var body = row[3]; // Col D: emailbody
var emailStatus = row[lastColumn - 1]; // Col E: Email Status
var range = sheet.getRange(2, 6, 1, 5).getDataRegion(SpreadsheetApp.Dimension.ROWS);
var values = range.getValues();
// Prevent from drafing duplicates and from drafting emails without a recipient
if (emailStatus !== EMAIL_DRAFTED && clientEmail) {
// Build the email message
var emailBody = '<p>Hi ' + clientName + ',<p>';
//emailBody += '<p>We are pleased to match you with your vegetable: <strong>' + veg + '</strong><p>';
//emailBody += '<h2>About ' + veg + '</h2>';
emailBody += '<p>' + values + '</p>';
// emailBody += '<p>' + clientName + ', we hope that you and ' + veg + ' have a wonderful relationship.<p>';
// Create the email draft
GmailApp.createDraft(
clientEmail, // Recipient
sub, // Subject
'', // Body (plain text)
{
htmlBody: emailBody // Options: Body (HTML)
}
);
sheet.getRange(startRow + i, lastColumn).setValue(EMAIL_DRAFTED); // Update the last column with "EMAIL_DRAFTED"
SpreadsheetApp.flush(); // Make sure the last cell is updated right away
}
}
}
What i am getting is by using this code:
Body
7 & 8,9 & 10,IOS,5 & 6,,a,b,c,d,,1,2,3,4,,1,2,3,4,,1,2,3,4,,1,2,3,4,,1,2,3,4,,1,2,3,4,,1,2,3,4,,1,2,3,4,
expected result should be like this.
This how the google sheet looks like.
Try this:
function draftMyEmails() {
var sh=SpreadsheetApp.getActiveSheet();
var startRow=2;
var lastCol=sh.getLastColumn();
var dataRange=sh.getRange(startRow,1,sh.getLastRow()-startRow+1,lastCol);
var data=dataRange.getValues();
for (var i=0;i<data.length;i++) {
var row=data[i];
var clientName=row[0];
var clientEmail=row[1];
var sub=row[2];
var body=row[3];
var emailStatus=row[lastCol-1];
var range=sh.getRange(2, 6, 1, 5).getDataRegion(SpreadsheetApp.Dimension.ROWS);
var values=range.getValues();
if (emailStatus!="EMAIL DRAFTED" && clientEmail) {
var emailBody= Utilities.formatString('<p>Hi %s </p>,<br />',clientName);
emailBody+='<style>td{border:1px solid black;)</style><table>';
values.forEach(function(r,j){
emailbody+='<tr>
r.foreach(function(c,k){
emailBody+=Utilities.formatString('<td>%s</td>',c);
});
emailBody+='</tr>';
});
emailBody+='</table>';
GmailApp.createDraft(clientEmail,sub,'',{htmlBody: emailBody});
sh.getRange(startRow + i, lastColumn).setValue("EMAIL_DRAFTED");
}
}
}
Please provide me with an image of your spreadsheet so I can see what your table looks like and I'll debug the table area for you or you can do it yourself.
I just started using Google Sheets and I'm having trouble when setting a script.
I need a cell (let's say "AF4") to get the last modification date of another cell (let's say "X4").
Here's my script so far:
`function onEdit(e)
{
var sheet = SpreadsheetApp.getActiveSheet();
var editRange = sheet.getActiveRange();
var editRow = editRange.getRow();
var editCol = editRange.getColumn();
var range = sheet.getRange("X4");
var rangeRow = range.getRow();
var rangeCol = range.getColumn();
if (editRow = rangeRow && editCol = rangeCol)
{
sheet.getRange("AF4").setValue(new Date());
}
}`
When I try to save it, it says "invalid transfer on left side, line 1, "code" "
I'd really apprecite any help with that.
Thank you,
CURTY
The syntax for your condition seems to be wrong, that's why the error appears :)
if (editRow = rangeRow && editCol = rangeCol)
in case you want to check based on more conditions, the syntax must be
if (condition) {
block of code to be executed if the condition is true
if (condition) {
block of code to be executed if the condition is true
}
}`
I modified a bit of your code that you get an idea, of what needs to be done in order to achieve your goal, this script to get the last modification date of column X and post "new date" into AF4 (so whenever you edit something on column "x" and the row 4 the date will be set in AF4)
function onEdit() {
var sheet = SpreadsheetApp.getActiveSheet();
var editRange = sheet.getActiveRange();
var editRow = editRange.getRow();
var editCol = editRange.getColumn();
var editCell = sheet.getActiveCell();
var range = sheet.getRange("X4");
var rangeRow = range.getRow();
var rangeCol = range.getColumn();
if( sheet.getName() == "Sheet1" ) { //checks that we're on the correct sheet
if( editCell.getColumn() == 24 ) { //checks that we're on the correct column ; column x = 24
if( editCell.getRow() == 4 ) { //checks that we're on the correct row ; row 4 = 4
sheet.getRange("AF4").setValue(new Date());
SpreadsheetApp.getActive().toast('Data changed', 'Info', 2); //give a Info, that something was done correctly
}
}
}
}
Hope this will help you!
EDIT
Script from comments request, whenever something on Column X is changed, add Date to column AF in the same row
function onEdit() {
var sheet = SpreadsheetApp.getActiveSheet();
var editRange = sheet.getActiveRange();
var editCol = editRange.getColumn();
var editCell = sheet.getActiveCell();
var range = sheet.getRange("X4");
if( editCell.getColumn() == 24 ) {
var rangeCol = range.getColumn();
var editRow = editRange.getRow();
sheet.getRange(editRow,rangeCol+8).setValue(new Date()); //use the RangeCol + 8 Columns
SpreadsheetApp.getActive().toast('Date added', 'Info', 2); //this line can be deleted
}
}
I'm having some trouble getting the value of a column in a saved search via SuiteScript. Below is my code:
function KW_AutoCloseOldRA() {
var search = nlapiLoadSearch('transaction', 'customsearchopen_ras', null, null);
var columns = search.getColumns();
for (i = 0; i < columns.length; i++) {
nlapiLogExecution('DEBUG', 'Column #' + i + ' is ' + columns[i].getName());
}
var results = search.runSearch();
if (results) {
results.forEachResult(getResults);
}
}
function getResults(res) {
var message = res.getValue('tranid');
nlapiLogExecution('DEBUG', 'Result ' + message);
return true;
}
The search produces two columns, and the name of those columns output as expected in the DEBUG entry (internalid is column 0 and tranid is column 1). When looping through the results however, res.getValue('tranid') is always null. I can't seem to find what I'm doing wrong here.
Try getting the value using the columns object and its index like this:
function KW_AutoCloseOldRA() {
var search = nlapiLoadSearch('transaction', 'customsearchopen_ras', null, null);
var columns = search.getColumns();
for (i = 0; i < columns.length; i++) {
nlapiLogExecution('DEBUG', 'Column #' + i + ' is ' + columns[i].getName());
}
var results = search.runSearch();
if (results) {
results.forEachResult(getResults);
}
}
function getResults(res) {
var cols = res.getAllColumns();
var message = res.getValue(cols[1]);
nlapiLogExecution('DEBUG', 'Result ' + message);
return true;
}
So I have some code to create calendar events based on a jobs sheet for meetings but I can't seem to get it working as I would expect- I have columns 24 and 25 to keep track of if its been put in the calendar and the calendar event id, I don't want it to delete then create a new event for ones that have already been added (as this spreadsheet can get large) so thats why I keep track via on edit. But is seems to create a new event every time. If anyone can have a look over that would be great as I've been struggling for the past 3 days with this.
Many thanks
//push new events to calendar;
function pushToCalendar() {
//spreadsheet variables
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = sheet.getLastRow();
var range = sheet.getRange(2,1,lastRow,26);
var values = range.getValues();
var updateRange = sheet.getRange('Z1');
//calendar variables
var calendar = CalendarApp.getCalendarById('insert calendar code here')
//show updating message
updateRange.setFontColor('red');
var numValues = 0;
for (var i = 0; i < values.length; i++) {
//check to see if name are filled out
if ((values[i][0].length > 0) && (values[i][1].length > 0)) {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if it has been edited delete old event
if (values[i][23] ='n') {
try{
var eventIdCell =values[i][24];
var eventId =calendar.getEventSeriesById(eventIdCell);
eventId.deleteEventSeries();
}
catch (e) {
// do nothing - we just want to delete if it has been edited and the old event if it still exists
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//check if it's been entered before
if (values[i][23] !='y') {
var newEventTitle = values[i][0] + ' - ' + values[i][1]+' - ' + 'Sample';
var newEvent = calendar.createAllDayEvent(newEventTitle, new Date(values[i][6]));
//get ID
var newEventId = newEvent.getId();
//mark as entered, enter ID
sheet.getRange(i+2,24).setValue('y');
sheet.getRange(i+2,25).setValue(newEventId);
}
}
numValues++;
}
//hide updating message
updateRange.setFontColor('white');
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//add a menu when the spreadsheet is opened
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [];
menuEntries.push({name: "Update Calendar", functionName: "pushToCalendar"});
sheet.addMenu("Jobs Calendar", menuEntries);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
function onEdit(event){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var actSht = event.source.getActiveSheet();
var actRng = event.source.getActiveRange();
var activeCell = actSht.getActiveCell();
var row = activeCell.getRow();
if(row < 2){
return; //If header row then return
}
else{
var index = actRng.getRowIndex();
var updateCalCell = actSht.getRange(index,24);
var eventIdCell = actSht.getRange(index,25);
change updated on colander status to n
updateCalCell.setValue('n');
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
You made a simple error in the condition : the EQUAL operator in comparison is == and not =, change that and it will work.
if (values[i][23] =='n') {