Google Apps Script failing onFormSubmit with concurrent responses - google-apps-script

I've setup a Google Form and linked Sheet to record form responses and then create and email the respondent a letter from the submitted data. This has been working fine and has started getting quite popular among colleagues, but now a problem has arisen with onFormSubmit when two users submit the form at the same time.
Essentially, the script is setup to run on the last row of the sheet onFormSubmit but if two responses are submitted concurrently it only runs the script on the last entry, potentially twice.
I'm thinking it might be possible to get around this by setting up a column to be marked if the script completed and then setting up a time-drive trigger to run on any rows that haven't been marked but this feels a bit clunky as respondents usually require the letter immediately.
Is there another way to approach the problem so that onFormSubmit makes sure the script actually runs on the row created by the original form submission and not simply the last row? Any help would be much appreciated, here's an example of my code:
function createLetterFromForm(){
// Get data from sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
// Define range and data in each column
var data = sheet.getRange(sheet.getLastRow(), 1, 1,
sheet.getLastColumn()).getValues(); // Range (last entry submitted)
for (var i in data){
var row = data[i];
// Pick the right template
if (row[9]=="A"){
var templateid = "xxxxxxxxxxxx";} // Template 1
if (row[9]=="B"){
var templateid = "xxxxxxxxxxxy";} // Template 2
// Make copy and set active
var folder = DriveApp.getFolderById("zzzzzzzzzzzzzz") // Folder for generated letters
var docid = DriveApp.getFileById(templateid).makeCopy(row[7]+" - Letter",folder).getId();
var doc = DocumentApp.openById(docid);
var docBody = doc.getActiveSection();
// Copy data to template
// address
docBody.replaceText("%FNAME%", row[2]);
docBody.replaceText("%SNAME%", row[3]);
docBody.replaceText("%ADDL1%", row[4]);
docBody.replaceText("%ADDL2%", row[5]);
docBody.replaceText("%ADDL3%", row[6]);
docBody.replaceText("%PCODE%", row[7]);
// other data
docBody.replaceText("%DATA1%, row[8]);
// etc.
// Share and Save doc
doc.addEditor(row[1]);
doc.saveAndClose();
// Email PDF to Respondee
var sendFile = DriveApp.getFilesByName(row[7]+' - Letter');
var recipient = row[1]
MailApp.sendEmail({
to:recipient,
subject: "Your Letter",
body:"Hello, \n\nHere's a PDF copy of the letter you created.",
attachments: [sendFile.next()]
});
}
}

Related

Google Sheets Email Script - Sending different sheet tabs based on values of Column on main sheet

I'm trying to write a script that will allow me to email a list of 150 employees with their individual sales data for the week.
At the moment I have a main front sheet with a Column for Email, Subject, and Store number. Each store number correlates to a Sheet (tab) with the same name, for example joe#gmail.com at store number 5070 has a tab named '5070' with changing data.
The problem I'm having is referencing the changing variable sheet name.
function sendEmail() {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet1=ss.getSheetByName('Sheet1');
var n=sheet1.getLastRow();
for (var i = 2; i < n+1 ; i++ ) {
var emailAddress = sheet1.getRange(i,1).getValue();
var subject = sheet1.getRange(i,2).getValue();
var message = sheet1.getRange(i,3).getValue();
MailApp.sendEmail(emailAddress, subject, message);
}
}
I am very new to the whole thing and have been searching around but have not had much luck. Thank you in advance!
You can't send a sheet. You can send only a link to the sheet.
If you replace this:
var message = sheet1.getRange(i,3).getValue();
with this:
var sheet_name = sheet1.getRange(i,3).getValue();
var sheet = ss.getSheetByName(sheet_name);
var message = sheet.getUrl();
Your recipients will get the link to the spreadsheet (a whole sheet, not even to the particular sheet).
To send a link to a particular sheet of the spreadsheet you need a bit more complicated solution:
var sheet_name = sheet1.getRange(i,3).getValue();
var sheet = ss.getSheetByName(sheet_name);
var message = getSheetUrl(sheet);
function getSheetUrl(sht) {
// credits: https://webapps.stackexchange.com/questions/93305/
var ss = SpreadsheetApp.getActive();
var url = '';
sht = sht ? sht : ss.getActiveSheet();
url = (ss.getUrl() + '#gid=' + ss.getSheetId());
return url;
}
But all your recipients will see all the spreadsheet anyway with all its sheets. In case this is not the thing you want you have three options:
Option 1 -- Make a new spreadsheet, copy the data into it and send the link to this new spreadsheet.
Option 2 -- Make PDF from the sheet and send it. Actually you will need to perform Option 1 first, convert the new spreadsheet to PDF, and delete the new spreadsheet after you send it (as PDF).
Option 3 -- make a HTML table (or text table, why not?) from the data of the sheet and send the table.

Create a Google Doc for Changed Row on Sheet When Editing Form Responses

Here's a link to the build project: https://drive.google.com/open?id=1IbmEWv_y60n-IefIrjmTcND-E6Rv35vb
Goal
Create a document when the form is submitted initially. Then, create a new document for each affected row on the sheet "Form Responses 1" when using the form to edit responses.
Current Results
Documents are created for all rows on the sheet every time the script runs. The individual documents include the correct data, but documents are also created for rows that didn't change values.
Problem
I'm inexperienced with scripts and cannot find a way to create a document only if values within the row have changed.
I tried a method other than the script below using "On form submit" as a trigger, but editing responses resulted in creating documents that ignored any response that was left unedited. In other words, if the form collected data for Q1 and Q2, the first document created would include everything entered on the form, as expected. If I edited the form response for Q1, however, the newly created document ignored Q2 entirely and only displayed Q1.
I also tried using "On edit" as a trigger. That didn't work at all when using the form. It required editing the sheet "Form Responses 1" directly, so it didn't meet my needs.
Help?
Current Trigger and Script
Trigger: On form submit
function setUp() {
//IDs to get and open
var tid = "1Us7qyNjFTeTrrA2Xt_Yigks1Yn-n0KGre9rUn3UV5yc";
var fid = "1HVy7J7EsgKfX-BjgzOGgXlcmYDuLs5C_";
var sid = "1FrJpfE9L2ZRE76ABDWA0gfYaZd6RFkvGQkSipHFCZCU";
var sname = "Form Responses 1";
//Get the template doc
var template = DriveApp.getFileById(tid);
//Get the output folder
var folder = DriveApp.getFolderById(fid);
//Open the form response worksheet
var ws = SpreadsheetApp.openById(sid).getSheetByName(sname);
//Get the worksheet range
//starting row #, starting column #, last row, # of columns
var range = ws.getRange(2,1,ws.getLastRow()-1,7).getValues();
range.forEach(function(r){
//Name the data
var ts = r[0];
var email = r[1];
var q1 = r[2];
var q2 = r[3];
var q3 = r[4];
var q4 = r[5];
var q5 = r[6];
//Format the timestamp
var date = Utilities.formatDate(new Date(ts), "GMT-5", "yyyy-MM-dd");
createDoc(template,folder,date,email,q1,q2,q3,q4,q5);
});
}
function createDoc(template,folder,date,email,q1,q2,q3,q4,q5) {
//Copy the template, name the doc, and put it in the folder
var copy = template.makeCopy(q1 + ' - ' + date, folder);
//Open the new document
var doc = DocumentApp.openById(copy.getId());
//Get and update the body of the document
var body = doc.getBody();
body.replaceText('##Date##', date);
body.replaceText('##Email##', email);
body.replaceText('##Question1##', q1);
body.replaceText('##Question2##', q2);
body.replaceText('##Question3##', q3);
body.replaceText('##Question4##', q4);
body.replaceText('##Question5##', q5);
//Save and close the document to persist our changes
doc.saveAndClose();
}
You were in the right way when you were using the onFormSubmit(e) and yes, the e object gives you the edited cells, but not the other values, what I did to solve that issue was to use the range that brings the e object to locate the edited row and then find it using e.range.getRow(), the rest would be done as you were doing it.
// Create a new document with the submitted data in the sheet
function createDoc(data){
//Get the output folder
var folder = DriveApp.getFolderById("your folder ID");
//Get the template doc
var template = DriveApp.getFileById("your file ID");
//Copy the template, name the doc, and put it in the folder
var copy = template.makeCopy(data[0][2] + ' - ' + data[0][0], folder);
//Open the new document
var doc = DocumentApp.openById(copy.getId());
//Get and update the body of the document
var body = doc.getBody();
body.replaceText('##Date##', data[0][0]);
body.replaceText('##Email##', data[0][1]);
body.replaceText('##Question1##', data[0][2]);
body.replaceText('##Question2##', data[0][3]);
body.replaceText('##Question3##', data[0][4]);
body.replaceText('##Question4##', data[0][5]);
body.replaceText('##Question5##', data[0][6]);
//Save and close the document to persist our changes
doc.saveAndClose();
}
// This will be trigger every time the form is submitted
function onFormSubmit(e) {
// Get active sheet in the active spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var numberCols = 7;
// Get values in the row that was edited
var editedRow = sheet.getRange(e.range.getRow(), 1, 1, numberCols).getValues();
// Create a new doc
createDoc(editedRow);
}
As you can see, I made several changes to your code, you can use it like that or adapt it in the way you would want to. Just keep in mind in the future you could check the values inside the data array to verify if they are empty or not.
Docs
For more info about you can check these and be careful about the Instabble Triggers restriccions:
onFormSubmit().
Event Objects - Form Submit.
Class Range.
Installable Triggers.
I was just trying to provide a way to store your last configuration so that you could determinine whether a change has been made between triggers so that you would know whether or not to generate a new document.
SpreadsheetApp.getActive().getSheetByName('ConfigSheet').appendRow([tid,fid,date,email,q1,q2,q3,q4,q5]);
I'd do it this way because getting the last configure is as simple as
var sh=SpreadsheetApp.getActive().getSheetByName('ConfigSheet');
sh.getRange(sh.getLastRow(),1,1,sh.getLastColumn()).getValues()[0];`
and now you have all of your latest configuration setting in a flat array. But I don't know what sort of changes merits creating a new document.

How do I reduce the "computer time" that my Google Apps Script is using?

I followed Hugo Fierro's tutorial on adding a Google Apps Script to send emails from Google Sheets. The tut is at https://developers.google.com/apps-script/articles/sending_emails.
I customised the script:
1) I replaced the "MailApp.sendEmail" API with "GmailApp.sendEmail" because I was getting authentication errors and the emails were not sending. The Gmail API has worked fine.
2) I added an option to send a PDF attachment with each mail using "DriveApp.getFileById".
3) I added a second condition to the IF statement to check that the PDF document is available before sending (by referencing a column in the sheet).
The problem is that if the script references only 5 rows, then it processes in under 30 seconds. When I try to process 10 rows or more, the processing time goes up significantly.
I replaced "sheet.getRange" with "sheet.getLastRow()" in an attempt to reduce how many rows the script is referencing.
var READY = 'READY';
var SENT = 'SENT';
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Email'); // Get the active spreadsheet, then get the "Email" sheet
var startRow = 2; // Select data row to start at
var endRow = sheet.getLastRow(); // Get the last row in the sheet
var data = sheet.getRange(startRow, 1, endRow, 6).getValues(); // Get the range of cells, then get the values
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var email = row[0]; // Column 1
var subject = row[1]; // Column 2
var message = row[2]; // Column 3
var attachment = DriveApp.getFileById(row[3]); // Returns the attachment file ID
var emailReady = row[4]; // Column 5
var emailSent = row[5]; // Column 6
var name = 'VFISA'; // Set "from" name in email
var bcc = 'myaddress#gmail.com'; // Blind carbon copy this email address
if (emailReady==READY && emailSent!==SENT) { // Prevents sending duplicates, waits for attachment cell to confirm available
GmailApp.sendEmail(email, subject, message,{
name: name,
bcc: bcc,
htmlBody: message,
attachments: attachment
});
sheet.getRange(startRow + i, 6).setValue(SENT); // Set the cell in column F to "SENT"
SpreadsheetApp.flush(); // Make sure the cell is updated right away in case the script is interrupted
}
}
}
I expected the script to run much faster. The error I get is "Service using too much computer time for one day". When I reference 20 rows it takes up to 4 minutes to run.
DriveApp.getFileById was causing each row to take about 5 seconds to execute. I removed this and now the entire script executes in under 1 second.
Instead of adding the PDF as an attachment, I used an inline link based on the file's ID (based on Alan Wells' suggestion).

Run script on last line of google form responses

I'm new to Google Apps Script and I'm trying without luck to automate a previously manual process. The script is within a sheet which takes form responses, converts them to a document and then emails the respondent with a copy. Here's the code in question:
function createDocFromSheet(){
var templateid = "1E7zzpvDF0U66aNqJdkUqjONx4wQRarkcWDy28NVqafU"; // get template file id
var folder = DriveApp.getFolderById("1-8lae1z_Z-Sy1IczUyB2JvCqCBV8zB5D")// folder name of where to put completed quotes
// get the data from sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var data = sheet.getRange(2, 1, sheet.getLastRow()-1, sheet.getLastColumn()).getValues();
var username = Session.getActiveUser(); // get their email
var dateNow = new Date(); // get date, clumsily
var dateD = dateNow.getDate();
var dateM = dateNow.getMonth();
var dateY = dateNow.getYear();
// for each row, fill in the template with response data
for (var i in data){
var row = data[i];
// make copy and set active
var docid = DriveApp.getFileById(templateid).makeCopy(row[7]+" - Postal Quote",folder).getId();
var doc = DocumentApp.openById(docid);
var body = doc.getActiveSection();
// date - working
body.replaceText("%D%", dateD);
body.replaceText("%M%", dateM+1);
body.replaceText("%Y%", dateY);
// address - working
body.replaceText("%FNAME%", row[2]);
body.replaceText("%SNAME%", row[3]);
body.replaceText("%ADDL1%", row[4]);
body.replaceText("%ADDL2%", row[5]);
This is setup to trigger on form submit but instead of running the script on the last row it runs for all previous responses as well. This was fine previously when responses were copied to a separate sheet and processed in bulk but I'm lost with how to make it run on only the new response.
Can anyone point me in the right direction?
Line 8; var data = sheet.getRange(2, 1, sheet.getLastRow()-1, sheet.getLastColumn()).getValues(); in this line you are getting the range A2:whatever the last row and column is, that's why it's going over the entire range.
Changing it to var data = sheet.getRange(sheet.getLastRow()-1, 1, 1, sheet.getLastColumn()).getValues(); should work.
or you can use the e parameter of the onFormSubmit event.
var getSubmission = e.range.getValues();
Have a look at the documentation for the e parameter, it's can be difficult to use in the beginning when debugging but it's very useful when you get the hang of it.

TypeError: Cannot find function getCell in object Sheet

I am completely new to coding anything related to Google Apps Script and JavaScript in general.
I've adapted a script to my needs, but I am getting the following error when I run it:
TypeError: Cannot find function getCell in object Sheet
Essentially, I am trying to get the value in cell D4 (4,4) and pass that value to the variable emailTo. I'm obviously not doing it correctly. The rest of the script should work fine. Any guidance is appreciated.
// Sends PDF receipt
// Based on script by ixhd at https://gist.github.com/ixhd/3660885
// Load a menu item called "Receipt" with a submenu item called "E-mail Receipt"
// Running this, sends the currently open sheet, as a PDF attachment
function onOpen() {
var submenu = [{name:"E-mail Receipt", functionName:"exportSomeSheets"}];
SpreadsheetApp.getActiveSpreadsheet().addMenu('Receipt', submenu);
}
function exportSomeSheets() {
// Set the Active Spreadsheet so we don't forget
var originalSpreadsheet = SpreadsheetApp.getActive();
// Set the message to attach to the email.
var message = "Thank you for attending ! Please find your receipt attached.";
// Construct the Subject Line
var subject = "Receipt";
// THIS IS WHERE THE PROBLEM IS
// Pull e-mail address from D4 to send receipt to
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var emailTo = sheet.getCell(4, 4).getValue();
// Create a new Spreadsheet and copy the current sheet into it.
var newSpreadsheet = SpreadsheetApp.create("Spreadsheet to export");
var projectname = SpreadsheetApp.getActiveSpreadsheet();
sheet = originalSpreadsheet.getActiveSheet();
sheet.copyTo(newSpreadsheet);
// Find and delete the default "Sheet 1"
newSpreadsheet.getSheetByName('Sheet1').activate();
newSpreadsheet.deleteActiveSheet();
// Make the PDF called "Receipt.pdf"
var pdf = DocsList.getFileById(newSpreadsheet.getId()).getAs('application/pdf').getBytes();
var attach = {fileName:'Receipt.pdf',content:pdf, mimeType:'application/pdf'};
// Send the constructed email
MailApp.sendEmail(emailTo, subject, message, {attachments:[attach]});
// Delete the wasted sheet
DocsList.getFileById(newSpreadsheet.getId()).setTrashed(true);
}
The issue is that getCell() is a method of Range, not Sheet. Get a Range from the Sheet, then use getCell() on the Range object