Script onForm Submit trigger not working properly - google-apps-script

I created a google form that will take the latest response and move the selected choice to the other section. So if someone checks out a laptop, once they submit the form the laptop choice will appear in the check in section. When I manually run the script it works perfectly fine but once I add the trigger it works for the first few times then it starts creating multiple triggers for one submission which then creates multiple new checkboxes on the form that all say the same thing. Like for example I'll have three different laptop choices when there should only be one. So I had to take the trigger off and I've looked at other similar questions about this problem but they all involve spreadsheets but mine is purely working with the google form so I'm not sure if those solutions will work for me.
I didn't add all my code since part of it is the same thing just with different variables for moving choices from check in to checkout.
var form = FormApp.openById('1I5uMesHbeVZ_RSP8wxmmpPA7-Sgcc4b6dzzH305c8K8');
/**
Responds to a form submission event when the on formSubmit trigger is
enabled
*
* #param {Event} e The event parameter created by a form submission
*/
//function that gets checkout responses
function myFunction(e) {
//getting form responses
var formResponses = form.getResponses();
//getting latest response
var latestFR = formResponses[form.getResponses().length-1];
//getting the item/question responses, checkout check in
var itemResponses = latestFR.getItemResponses();
//looping through item responses to see which item has a response
for (var i=0; i<itemResponses.length; i++) {
switch (itemResponses[i].getItem().getTitle()) {
//if only response to checkout
case "Checkout":
var outAnswer = itemResponses[i].getResponse();
outAnswer.forEach(addOut);
outAnswer.forEach(deleteOut);
break;
//if only response to check in
case "Check In":
var inAnswer = itemResponses[i].getResponse();
inAnswer.forEach(addToCheckOut);
inAnswer.forEach(deleteIn);
break;
//if response to both check out/in
case "Checkout" && "Check In":
var outAnswer = itemResponses[i].getResponse();
var inAnswer = itemResponses[i].getResponse();
outAnswer.forEach(addOut);
outAnswer.forEach(deleteOut);
inAnswer.forEach(addToCheckOut);
inAnswer.forEach(deleteIn);
break;
}}
//getting email response to send email
var email = itemResponses[0].getResponse();
//testing to see if it gets the latest submission
//delete my email later
var subject = 'Response';
var emailTo = [email];
var body = 'Response is' + outAnswer + inAnswer;
MailApp.sendEmail(emailTo, subject, body, {
htmlBody: body});
}
//function that adds the latest response from checkout to check in
section
function addOut(outAnswer) {
//getting check in section item with its choices
var a = form.getItems(FormApp.ItemType.CHECKBOX)[1].asCheckboxItem();
//getting choices from check in
var choices = a.getChoices();
//creating new choice for check in
var choice = a.createChoice(outAnswer);
//adding the choice to the choices
choices.push(choice);
//setting the choices with new choice for check in
a.setChoices(choices);
}
//function that deletes answer from checkout
//only works when its a string so convert outAnswer to string value with
toString but only works with a single choice
function deleteOut(outAnswer) {
var del = form.getItems(FormApp.ItemType.CHECKBOX)
[0].asCheckboxItem();
del.setChoices(del.getChoices().filter(function (choice) {
return choice.getValue() !== outAnswer.toString(); }));
}

You're going to need to do the same kind of thing as the spreadsheet answers suggested, create a script lock and use it to dump quick successive triggers.
Just add the following lines to the top of your script:
var lock = LockService.getScriptLock();
try {
lock.waitLock(3000);
} catch (e) {Logger.log('Could not obtain lock after 3 seconds.');}
Utilities.sleep(3000);
You can also add a "lock.releaseLock();" to the end of your script, but it isn't necessary, locks release on their own.
All this code does is reject any new submissions in the next three seconds after it is triggered. If that isn't enough, change the time in the waitlock AND the sleep to 5000 (forms generally take less than three seconds to run a script like this so you are forcing the script to take longer).

Related

Google sheet sends an email when a new form is submitted if a question has a particular answer

I hope you can help, I have tried to find a solution but I am unable to find one that resolves my issue.
I have a form that collects information if a student request a change to their course, we are a multi school site and depending on the site, a different member of staff is required to approve the change.
Part of the information collect is the site code OAAd for example, and this information is held in column F.
If a submission is received the 'On form submit' trigger, triggers the script. Currently it doesn't seem to work, yet it triggers without error. If I change the trigger 'On edit' and edit a cell in column F it works fine.
eventually I will use ifElse to add in the additional options for each site, but if I could get it to work for one site that would be great.
function sendEmailapproval(e) {
if(e.range.getColumn()==6 && e.value=='OAAd'){
var emailRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Email Addresses").getRange("B3");
var recipent = emailRange.getValue();
var html = HtmlService.createTemplateFromFile("Email1.html");
var htmlText = html.evaluate().getContent();
var subject = "New Change Log Request Post 16";
var body = "";
var options = { htmlBody: htmlText}
MailApp.sendEmail(recipent, subject, body, options)
}
}
Background
There are 2 options to add an onFormSubmit trigger:
Using the form settings / script.
Using the attached spreadsheet script.
Both methods gives an event object to communicate with new submmited responese, but threre are differaces between those objet so you need to dicide in advance which method to choose.
You can read more about this topic here.
For your propose, would preder to use the spreadsheet script, since it easier to extract specific values from it.
Solution
First, Here is your correct sendEmailapproval function:
function sendEmailapproval(e) {
const namedValues = e.namedValues
const question = 'Question 3'; // copy-paste from the form
switch (namedValues[question][0])
{
case 'OAAd':
const emailRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Email Addresses").getRange("B3");
const recipent = emailRange.getValue();
const html = HtmlService.createTemplateFromFile("Email1.html");
const htmlText = html.evaluate().getContent();
const subject = "New Change Log Request Post 16";
const body = "";
const options = { htmlBody: htmlText}
MailApp.sendEmail(recipent, subject, body, options)
break;
// case 'SOMETHING_ELSE':
// break;
default:
}
}
Note that e.namedValues is an object, where each key mapped to an array.
So namedValues[question][0] refers to the string value of namedValues[question].
Second, add a trigger to the spreadsheet that attached to the form.
function createTrigger() {
const sheet = SpreadsheetApp.openById("YOUR_SPREADSHEET_ID").getSheetByName("THE_SHEET_NAME");
ScriptApp.newTrigger("sendEmailapproval")
.forSpreadsheet(sheet)
.onFormSubmit()
.create();
}

Using Google Apps Script to get responses from multiple forms and send an email once to each respondent

I have been puzzling this over for some time and have searched extensively, but found no solution.
I'm using Google Apps Script and I run events for a large organization and we have about 80 different registration Google Forms for these events. I want to get the registrant's email address and send them an email when they submit their form. This is easy to accomplish by setting up each individual form. Ideally, I would set up the onSubmit trigger for the form and then copy that form for each new event. However, it seems you cannot install a trigger programmatically without going to the form and running the script manually, and then authorize it. When a form is copied it also loses all its triggers. Am I wrong about this? Doing this for each form is not realistic given that events are added all the time and I have other responsibilities.
Is there no way to set a trigger for these files without running and authorizing each one?
My other solution is:
I am trying to get all the forms in a folder and then get the responses and send a single email to each registrant. This seems overly complicated and requires checking all the forms regularly since there are no triggers for the individual forms. I tried setting triggers in my spreadsheet for the forms and this works, but the number of triggers for a spreadsheet is limited to 20, so doesn't work here. Running my script every minute and then checking if an email has been sent to each respondent seems possible, but complex and possibly prone to errors...
Thanks for any help you can offer!
pgSystemTester's answer worked for me.
I added two bits of code.
One, to declare the time stamp value to zero if there wasn't one
there.
Two, the code needed a "-1" when you get dRange or you insert a new
row which each run.
function sendEmailsCalendarInvite() {
const dRange = sheet.getRange(2, registrationFormIdId, sheet.getLastRow() - 1, 2);
var theList = dRange.getValues();
for (i = 0; i < theList.length; i++) {
if (theList [i][1] == ''){
theList[i][1] = 0;
}
if (theList[i][0] != '') {
var aForm = FormApp.openById(theList[i][0]);
var latestReply = theList[i][1];
var allResponses = aForm.getResponses();
for (var r = 0; r < allResponses.length; r++) {
var aResponse = allResponses[r];
var rTime = aResponse.getTimestamp();
if (rTime > theList[i][1]) {
//run procedure on response using aForm and aResponse variables
console.log('If ran')
if (rTime > latestReply) {
//updates latest timestamp if needed
latestReply = rTime;
}
//next reply
}
}
theList[i][1] = latestReply;
//next form
}
}
//updates timestamps
dRange.setValues(theList);
}
This is probably a simple solution that will work. Setup a spreadsheet that holds all Form ID's you wish to check and then a corresponding latest response. Then set this below trigger to run every ten minutes or so.
const ss = SpreadsheetApp.getActiveSheet();
const dRange = ss.getRange(2,1,ss.getLastRow(),2 );
function loopFormsOnSheet() {
var theList = dRange.getValues();
for(i=0;i<theList.length;i++){
if(theList[i][0]!=''){
var aForm = FormApp.openById(theList[i][0]);
var latestReply = theList[i][1];
var allResponses = aForm.getResponses();
for(var r=0;r<allResponses.length;r++){
var aResponse = allResponses[r];
var rTime = aResponse.getTimestamp();
if(rTime > theList[i][1]){
//run procedure on response using aForm and aResponse variables
if(rTime >latestReply){
//updates latest timestamp if needed
latestReply=rTime;
}
//next reply
}
}
theList[i][1] = latestReply;
//next form
}
}
//updates timestamps
dRange.setValues(theList);
}

getMessageById() slows down

I am working on a script that works with e-mails and it needs to fetch the timestamp, sender, receiver and subject for an e-mail. The Google script project has several functions in separate script files so I won't be listing everything here, but essentially the main function performs a query and passes it on to a function that fetches data:
queriedMessages = Gmail.Users.Messages.list(authUsr.mail, {'q':query, 'pageToken':pageToken});
dataOutput_double(sSheet, queriedMessages.messages, queriedMessages.messages.length);
So this will send an object to the function dataOutput_double and the size of the array (if I try to get the size of the array inside the function that outputs data I get an error so that is why this is passed here). The function that outputs the data looks like this:
function dataOutput_double(sSheet, messageInfo, aLenght) {
var sheet = sSheet.getSheets()[0],
message,
dataArray = new Array(),
row = 2;
var i, dateCheck = new Date;
dateCheck.setDate(dateCheck.getDate()-1);
for (i=aLenght-1; i>=0; i--) {
message = GmailApp.getMessageById(messageInfo[i].id);
if (message.getDate().getDate() == dateCheck.getDate()) {
sheet.insertRowBefore(2);
sheet.getRange(row, 1).setValue(message.getDate());
sheet.getRange(row, 2).setValue(message.getFrom());
sheet.getRange(row, 3).setValue(message.getTo());
sheet.getRange(row, 4).setValue(message.getSubject());
}
}
return;
};
Some of this code will get removed as there are leftovers from other types of handling this.
The problem as I noticed is that some messages take a long time to get with the getMessageById() method (~ 4 seconds to be exact) and when the script is intended to work with ~1500 mails every day this makes it drag on for quite a while forcing google to stop the script as it takes too long.
Any ideas of how to go around this issue or is this just something that I have to live with?
Here is something I whipped up:
function processEmails() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var messages = Gmail.Users.Messages.list('me', {maxResults:200, q:"newer_than:1d AND label:INBOX NOT label:PROCESSED"}).messages,
headers,
headersFields = ["Date","From","To","Subject"],
outputValue=[],thisRowValue = [],
message
if(messages.length > 0){
for(var i in messages){
message = Gmail.Users.Messages.get('me', messages[i].id);
Gmail.Users.Messages.modify( {addLabelIds:["Label_4"]},'me',messages[i].id);
headers = message.payload.headers
for(var ii in headers){
if(headersFields.indexOf(headers[ii].name) != -1){
thisRowValue.push(headers[ii].value);
}
}
outputValue.push(thisRowValue)
thisRowValue = [];
}
var range = ss.getRange(ss.getLastRow()+1, ss.getLastColumn()+1, outputValue.length, outputValue[0].length);
range.setValues(outputValue);
}
}
NOTE: This is intended to run as a trigger. This will batch the trigger call in 200 messages. You will need to add the label PROCESSED to gmail. Also on the line:
Gmail.Users.Messages.modify( {addLabelIds:["Label_4"]},'me',messages[i].id);
it shows Label_4. In my gmail account "PROCESSED" is my 4th custom label.

Script runs twice on google form submit trigger

I'm create Google form and google app script with sendFormByEmail function,
also I set on form submit trigger for this function,
my issue is this script run two time on form submit and I'm getting two email,
I want only single email on form submit. my script code is below.
var no_repeat=0;
function sendFormByEmail(e){
var email = "test#XXXXDXtest.com";
var s = SpreadsheetApp.getActiveSheet();
var headers = s.getRange(1,1,1,s.getLastColumn()).getValues()[0];
var message = "";
var subject = "Success Assessment";
var total=0;
var roll_on=0;
message+="test massage";
message+="<table cellpadding='3' style='color: #0F1F4C;'>";
for(var i in headers) {
if(headers[i]=='Please enter your email address to receive your results'){
email=e.namedValues[headers[i]].toString();
}
if(headers[i]!='Please enter your email address to receive your results'){
if(headers[i]!='Timestamp'){
if(e.namedValues[headers[i]]!=''){
total = parseInt(e.namedValues[headers[i]])+parseInt(total);
}
message +="<tr >";
message += '<td >'+headers[i]+'</td><td >'+e.namedValues[headers[i]].toString()+ "</td>";
message +="</tr>";
roll_on++;
}
}
}
message +="<tr >";
message += "<td ><b> YOUR SCORE </b></td><td ><b>"+total+"</b></td>";
message+="</tr></table>";
// Send the email
if(email!='' && email!=null){
if(no_repeat==0){
MailApp.sendEmail(email, subject,"",{htmlBody: message});
}
no_repeat++;
}
}
This is a known issue at Google's end and they are working on a fix:
Our engineering team is working on the issue, but we don't have any
estimates as to when it will be fixed. I advise applying the
LockService logic as shown in update #22, which should work around the
problem.
I had same issue. The problem was that two users set up the same on submit trigger on the Google Form. I signed in as one of the users and deleted the trigger. I signed in as the other user and the trigger was still there. Works perfectly now, only runs once.
I've had the same issue on my spreadsheet, apparently it's a glitch the developers are trying to solve.
I have a similar spreadsheet that runs fine, however it was developed before the last update on the page that manage triggers.
Anyway, as a work around, I've created an extra column on my spreadsheet to ensure the script only runs once for each line, adding two code lines, the first to setvalue to the new column with 'OK' and an if to check that column
Hope it helps!
Att.
Looks like this still hasn't been fixed by the GAS team yet.
After spending weeks trying to get to the bottom of random glitches occurring in our script, we finally found this post. Very frustrating!
We found a simple variable check onSubmit to be an effective workaround:
function handleSubmit() {
if (window.formSubmitted !== undefined) {
return false;
}
window.formSubmitted = true;
console.log("Should never fire twice!");
google.script.run...
}
For a Form Trigger in Google Sheets I've tried this workaround: check the number of row to prevent 2 rows at once:
will also try 'Lock` if this does not work.
My code:
// chech that the row was not affected by trigger earlier
function getSameRowCheck_(row) {
var c = CacheService.getScriptCache();
var key = 'somekey'
var mem = c.get(key);
if (!mem) {
return true;
}
if (parseInt(mem) >= row) {
return false;
}
c.put(key, row, 21600 );
return true;
}
Use it in a trigger:
function onFormSubmit(e) {
var range = e.range;
var row = range.getRow();
var check = getSameRowCheck_(row);
if (!check) {
console.log('trigger for the row is running...');
return;
}
// do my job
}

Copying google apps script with forms

I'm running into a strange issue that is causing me considerable difficulty hoped someone her might have some insight.
I have a form that is accepting responses and sending the results to a sheet. I've written an additional onsubmit script that then marks up some other sheets both in order to facilitate tracking and to provide a prefilled url that can be sent next time the form needs to be submitted.
The forms are for student data collection and I therefor need three each collecting data about students in different years. My issue is that I can't seem to copy the scripts to the other forms without them ceasing to work.
I have taken the following approaches:
Initially I just tried duplicating the entire form and then changing it, this meant the scripts came along with the copy.
I then tried keeping the duplicated form but deleting out all the script information and copy and pasting the script to a new project within the form.
I then tried creating a new test form and copy and pasting the script to that - again no success.
All attempts have resulted in the data being successfully submitted to the responses sheet but the script either not triggering or failing silently.
I can't see anything in the script I have produced that would cause it to work only on one form but I'm not very experienced in JS and it may be I am not seeing something obvious ( I am aware there are some foolish bits in the script but this is the version I know works).
If anyone can point out anything that would make the script not work for any other forms or can explain how the script can be successfully copied I'd be really grateful.
Code:
/* onsubmit function that creates a prefilled url and prints to sheet */
function onSubmit(e) {
var subject = "ICT";
//generate url
var url=e.response.toPrefilledUrl();
var responses = e.response;//e is of type formresponse.
var name = responses.getItemResponses()[0].getResponse();
var sheet1 = SpreadsheetApp.openByUrl("https://docs.google.com/a/rbair.org.uk/spreadsheets/d/1w_rCPJR-O9_fUs1T5HKmaTUsRjk_9JRVRPxV2kJzNMk/edit#gid=0").getSheetByName("Admin");
update(name, subject);
//print to cell in feedback sheet
var data = sheet1.getDataRange().getValues();
for(i=0; i<100; i++)
{
if(data[i][1]==name)
{
sheet1.getRange(i+1, 3).setValue(url);
}
}
}
function test()
{
var name = "Piers Hartley";
var subject = "ICT";
update(name, subject);
}
function update (name, subject)
{
//var name = "Piers Hartley";
//var subject = "ICT";
var msheet = SpreadsheetApp.openByUrl("https://docs.google.com/a/rbair.org.uk/spreadsheets/d/1IauIkNCrE95qNAL2KxfJhespEcRWuMXYGzkkjwFnezg/edit#gid=0");
var refsheet = msheet.getSheetByName("Subject Teachers");
var track = refsheet.getRange(2,10).getValue();
var tsheet = msheet.getSheetByName(track);
var trdr = tsheet.getDataRange();
var trdata = trdr.getValues();
var lcol = trdr.getLastColumn();
var lrow = trdr.getLastRow();
for(i=0; i<lcol; i++)
{
if (trdata [0] [i] == subject)
{
for (j=1; j<lrow; j++)
{
if (trdata [j] [i] == name)
{
tsheet.getRange(i+2, j+1).setValue("Done");
}
}
}
}
}
Have you added the On Form Submit trigger? (under Resources in the script editor).