Ensure form submission trigger runs only one sheet - google-apps-script

I have two sheets in a particular spreadsheet both of which have form submissions tied to them. The first sheet/form is supposed to send an email containing the form submission data (the original was created by Amit Agarwal, here's an historic link). The second form/sheet doesn't do anything special as it just collects data from the form. The script in question is set to a On Form Submit trigger.
The issue I am having is that the script sometimes runs from form/sheet2. I would like to specify which sheet/form the script needs to be triggered from to run on. The modified code that I have created was based on lots of looking around. Here is the snippet:
function Initialize() {
var triggers = ScriptApp.getProjectTriggers();
for (var i in triggers) {
ScriptApp.deleteTrigger(triggers[i]);
}
ScriptApp.newTrigger("SendConfirmationMail")
.forSpreadsheet(SpreadsheetApp.getActiveSpreadsheet())
.onFormSubmit()
.create();
}
function SendConfirmationMail(e) {
try {
var ss, bcc, sendername, subject, columns;
var message, value, textbody, sender;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Help Request Tickets');
var rowNumber = s.getActiveRange().getRowIndex();
var row = e.range.getRow();
// This is your email address and you will be in the BCC
bcc = "email", "email";
// This will show up as the sender's name
sendername = "sendername";
// Optional but change the following variable
// to have a custom subject for Google Docs emails
subject = "subject"
// This is the body of the auto-reply
message = "message"
ss = SpreadsheetApp.getActiveSheet();
columns = ss.getRange(1, 1, 1, ss.getLastColumn()).getValues()[0];
These two lines I though were supposed to accomplish this:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Sheet1');
I suppose the script thinks Sheet2 is active (which if I have the spreadsheet open could be the case). Surely there is way to work around/accomplish this, what I am missing?

A Sheets form submission trigger will be invoked for all forms submitted to the spreadsheet. Once upon a time, only one form could be associated with a spreadsheet, but now, with multiple form associations possible, you need to allow for that possibility. You can't specify which form a trigger function is for, but you can check the source of the event and respond appropriately.
One effective way to do this is to use a director function which will receive all form submission events, and direct them to unique trigger functions depending on which sheet received the response.
Here, we are associating "Form Responses 1" with SendConfirmationMail(), and assuming that "Form Responses 2" has its own form submission handler, handleForm2(). (If there is no handler for that form, then the specific case can be deleted, and submissions will end up in the default case.)
/**
* This director function should be used as the "top level" form submission trigger
* function for spreadsheets accepting responses from multiple forms. Events are
* directed to the appropriate trigger sub-functions according to the name of the
* sheet which received the current response.
*
* From: https://stackoverflow.com/a/37839189/1677912
*/
function formSubmitted(e) {
var sheetName = e.range.getSheet().getName();
switch (sheetName) {
case "Form Responses 1":
SendConfirmationMail(e);
break;
case "Form Responses 2":
handleForm2(e);
break;
default:
// do nothing
break;
}
}
If you use a Forms form submission trigger instead, you can avoid this altogether, since the destination spreadsheet would not be a direct consideration.
I suppose the script thinks Sheet2 is active (which if I have the spreadsheet open could be the case)
Not quite. The trigger function is invoked outside of the context of any spreadsheet UI, so what any user is doing in the spreadsheet has no effect on it. Rather, the "active" sheet is related to the submission event being handled. Regardless, it is a much better idea to reference the event object itself, rather than rely on "normal" operations. It does become trickier to test and debug, but not terribly so. For more about testing trigger functions, see How can I test a trigger function in GAS?

Related

is there a way to get form responses and place them in specific cell in another sheet

I have set up a script that creates a Google form and links it to a spreadsheet, is there a way to collect the responses and place them in certain cells in another sheet then unlinks and deletes the form response sheet.
the script I need will have to be flexible enough that it won't matter what the response sheet is named as I will be making multiple one use forms hence why I would also like to delete the response sheet after the answer is moved
for example say the answer is A (a1), B (b1) and C (c1) and I want to move it to sheet 'C' and into columns F, G and H after that's done i would like the response sheet to unlink and be deleted
any help would be greatly appreciated
Issue:
You want form response data to be submitted to a sheet of your choice, not the one that is created when linking the form to the spreadsheet.
Solution:
In that case, I'd suggest not linking the form to the spreadsheet at all, and use an onFormSubmit trigger to write the submitted data to your desired sheet.
Workflow:
Install an onFormSubmit trigger. You can do that manually, following these steps, or programmatically, by executing this function once:
const SOURCE_FORM_ID = "YOUR_FORM_ID"; // Change according to your needs
function installOnFormSubmitTrigger() {
const form = FormApp.openById(SOURCE_FORM_ID);
ScriptApp.newTrigger("onFormSubmitTrigger")
.forForm(form)
.onFormSubmit()
.create();
}
Once the trigger is installed, a function named onFormSubmitTrigger (it doesn't have to be named that way) will execute every time someone submits a response to the form. This function should append the response data to your desired sheet. It could be something like this (check inline comments):
const TARGET_SPREADSHEET_ID = "YOUR_SPREADSHEET_ID"; // Change according to your needs
const TARGET_SHEET_NAME = "Sheet1"; // Change according to your needs
function onFormSubmitTrigger(e) {
const targetSpreadsheet = SpreadsheetApp.openById(TARGET_SPREADSHEET_ID);
const targetSheet = targetSpreadsheet.getSheetByName(TARGET_SHEET_NAME);
if (targetSheet.getLastRow() === 0) { // Add headers if they don't exist yet
const itemTitles = e.source.getItems().map(item => item.getTitle()); // Get item titles
itemTitles.unshift("Timestamp"); // Append "Timestamp" to the sheet (if desired)
targetSheet.appendRow(itemTitles); // Append form item titles to the sheet
}
const itemResponses = e.response.getItemResponses();
const responses = itemResponses.map(itemResponse => itemResponse.getResponse()); // Get user responses
responses.unshift(new Date()); // Add today's date to the responses (if desired)
targetSheet.appendRow(responses); // Append responses to the sheet
}
Note:
If you don't want to submit to the first columns in the spreadsheet, simply add empty strings to the responses array, or use Range.setValues instead.
Reference:
Installable Triggers
appendRow

Auto send Email from Sheets Using Google Scrip

I am trying to automate an email system where whenever someone submits a form, therefore editing the spreadsheet, google-scripts will send me an email containing the message. The code works perfectly fine and does what I want. However, I need to manually run the script every time which defeats the purpose. I have tried to do the function onEdit(e) however it never worked. I have also tried the triggers feature of AppsScript with no luck.
/**
* Sends emails with data from the current spreadsheet.
*/
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = sheet.getLastRow();
var numRows = sheet.getLastRow();
// Fetch the range of cells
var dataRange = sheet.getRange(startRow, 1, numRows, sheet.getLastColumn());
// Fetch values for each row in the Range.
var data = dataRange.getValues();
var emailAddress = 'email#email.com'; // First column
var message = data[0][3]; // Fourth column;
var subject = 'A Subject';//this will always be the same
Logger.log(emailAddress, subject, message); //This is to test because of the limit of how many emails you can send a day.
MailApp.sendEmail(emailAddress, subject, message);
}
Here is a link to a google sheet that you can test and edit the script on: https://docs.google.com/spreadsheets/d/1bBNDc33fBx2JPcRByt-8TA2vrLqa51H72TIM-SeARsc/edit?usp=sharing
The sheets is not using google forms, it is getting it's data from the results of an HTML webpage I built that is used as a form.
This is possible by using Installable Triggers.
Follow these steps once you created the form inside your sheets:
For New Editor
Open your Apps Script project.
At the left, click Triggers alarm.
At the bottom right, click Add Trigger.
Select and configure the type of trigger you want to create.
Click Save.
For Legacy Editor
From the script editor, choose Edit > Current project's triggers.
Click the link that says: No triggers set up. Click here to add one now.
Under Run, select the name of function you want to trigger.
Under Events, select either Time-driven or the Google App that the script is bound to (for example, From spreadsheet).
Select and configure the type of trigger you want to create (for example, an > > - Hour timer that runs Every hour or an On open trigger).
Optionally, click Notifications to configure how and when you are contacted by email if your triggered function fails.
Click Save.
For this scenario, Select event source: From spreadsheet and Select event type: On form submit.
Once the setup for Installable trigger is done, you can now add an event object e to the function parameter to access the form values submitted by the user.
Try this in your code:
function onSubmitForm(e){
var message = e.namedValues["Message"]; //get the message value using question name
var emailAddress = 'xxxxxsampleemailxxxxxx';
var subject = 'A Subject';
MailApp.sendEmail(emailAddress, subject, message); //send email
}
Example:
Output:
References:
Installable Triggers
Event Object

Sending emails twice with MailApp.sendEmail

I have created a google apps script to automate emails once a Google Form is submitted. The script is quite simple:
function AutoConfirmation(e){
var theName = e.values[1];
var theEmail = e.values[2];
var theSubject= e.values[3];
var myEmail = "myemail#gmail.com";
var theMessage = e.values[4];
var subject = "Contact form response – " + theSubject;
var message = theMessage;
MailApp.sendEmail (myEmail, subject,message);
}
However, fo some reason I can't figure out, every time a form is submitted I get two instant emails:
Has the data submitted (all works as expected)
Is empty (e.g. subject is "Contact form response –")
I even started from scratch in a different Google account I have and the same issue happens.
Appreciate any suggestions!
It appears the issue is being caused by the internal process that syncs form responses to the spreadsheet. Under some circumstances it makes slight updates to the "Timestamp" column of previously submitted form responses, which cause the onFormSubmit triggers to fire again for those rows (albeit with incomplete event objects).
The engineering team is still working on a fix, but in the mean time you can work around this issue by filtering out form submit events that only affect the timestamp column. Since you can reorder the columns in a Form Responses sheet, the best way would be to check if the event's range only covers a single column:
function onFormSubmit() {
if (e.range.columnStart == e.range.columnEnd) return;
// The rest of your code
// ...
}

e.range.getA1Notation() unable to track changes caused by formula update

I modified a script provided from this blog
How to have your spreadsheet automatically send out an email when a cell value changes
After some debugging an modifications, I can send emails by manually entering a value at position C7. That is, according to script, if the value is greater than 100, it will send a email to me. That only happens if I type the number manually into the cell.
The problem is, if the value is generated by a formula, then it doesn't work. (Say cell C7 is a formula=C4*C5 where the product value is >100)
After some trial-and-error, I think it is the code in the edit detection part causing the problem.
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
Since cell C7 is a formula, the formula itself doesn't change, what is changing is the values from formula calculations. So it may not think I have edited the cell.
How should I modify the script, so that the script also send email when value of C7 produced by a formula is greater than 100?
For reference, here is the code that I am using.
function checkValue(e)
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
{
if(valueToCheck >100)
{
MailApp.sendEmail("h********#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
}
}
}
onEdit(e) Trigger(Both simple and Installable) will not trigger unless a human explicitly edits the file. In your case, Your seem to be getting value from an external source (specifically, Google finance data).
Script executions and API requests do not cause triggers to run. For example, calling FormResponse.submit() to submit a new form response does not cause the form's submit trigger to run.
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause the spreadsheet's onEdit trigger to run.
Also, for Google finance data,
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you will see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
Notes:
Having said that,
In cases where the change is made by a formula(like =IF(),=VLOOKUP()) other than auto-change formulas(like =GOOGLEFINANCE,=IMPORTRANGE,=IMPORTXML, etc), Usually a human would need to edit some other cell- In which case, You will be able to capture that event(Which caused a human edit) and make changes to the formula cell instead.
In cases where the change is done from sheets api, a installed onChange trigger may be able to capture the event.
To avoid repeated notifications due to edits in other places, send an email only when the value you are tracking is different from the previous one. Store the previous value in script properties.
function checkValue(e) {
var sp = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var oldValue = sp.getProperty("C7") || 0;
if (valueToCheck > 100 && valueToCheck != oldValue) {
MailApp.sendEmail("***v#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
sp.setProperty("C7", valueToCheck);
}
}
Here the email is sent only when valueToCheck differs from the stored value. If this happens, the stored value is updated by setProperty.

Reject Google Forms submission or remove response

I'm using the current, "new" version of Google Forms. I have a form, Form A, and the associated script Script A. That script contains a function, onFormSubmit, associated with Form A's form submit trigger. The function receives one argument, an event, which contains the Form and the submitted FormResponse in the fields "source" and "response" respectively.
In the body of this function, how can I prevent the event/reject the form submission?
Alternatively, if this is not possible, how can I quietly prevent the FormResponse from being stored, or quietly remove it from the list of responses?
I see there is a Form.deleteAllResponses method. Do I have to delete all responses and then add all responses back again, except for the current one? Or is there a better way?
Try experimenting with the event trigger and use:
setAcceptingResponses(enabled)
Sets whether the form is currently accepting responses. The default for new forms is true.
Here is a code sample from a related SO post:
function onFormSubmit(){
var af = FormApp.getActiveForm();
var defaultClosedFor = af.getCustomClosedFormMessage();
af.setCustomClosedFormMessage("The form is currently processing a submission, please refresh the page.");
af.setAcceptingResponses(false);
<put your script stuff here>
af.setAcceptingResponses(true);
af.setCustomClosedFormMessage(defaultClosedFor);
}
Hope this helps.
The way I have handled this is by having a second sheet (tab within the same Google Sheet) into which Form responses are copied. The first thing that happens in the onFormSubmit() function is that the newest row in the responses sheet is copied to the duplicate sheet. You could implement a selection statement that chooses whether to make the copy or not depending on your criteria.
This means that there is always a copy of the raw responses from the form (important for auditing purposes) but also a means for correcting/modifying responses if errors were made by the submitter.
In case it is useful, this is my function that does the copy (note that my settings object is abstracted elsewhere but hopefully there is enough to make clear what is going on).
/**
* Copies form submissions from the responses sheet to another sheet.
*
* #param {event object} e the event object received from a form submit trigger
* #param {Settings} settings an object containing the settings this function expects to use
* #return {integer} the position of the new row in the destination sheet
*/
function copyFormSubmissionToSheet(e, settings) {
var sourceSheet = SpreadsheetApp.getActive().getSheetByName(settings.nameOfFormResponsesSheet);
var destinationSheet = SpreadsheetApp.getActive().getSheetByName(settings.name OfApprovalsSheet);
var newRow = e.range["rowStart"];
var columnCount = sourceSheet.getLastColumn();
var newResponseRange = sourceSheet.getRange(newRow, 1, 1, columnCount);
var newResponseDestinationRange = destinationSheet.getRange(destinationSheet.getLastRow()+1, 1, 1, columnCount);
newResponseRange.copyTo(newResponseDestinationRange);
var newDataSheetRow = destinationSheet.getLastRow();
return newDataSheetRow;
}