Google Trigger for Edited Form Entry - google-apps-script

I have a trigger that places the Edit Response URL in the 100th column.
Not sure who i plagiarized this from, please feel free to cite yourself, lol
function assignEditUrls() {
var form = FormApp.openById('XXXXXXXXXXXXXX');
//enter form ID here
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Current');
//Change the sheet name as appropriate
var data = sheet.getDataRange().getValues();
var urlCol = 100; // column number where URL's should be populated; A = 1, B = 2 etc
var responses = form.getResponses();
var timestamps = [], urls = [], resultUrls = [];
for (var i = 0; i < responses.length; i++) {
timestamps.push(responses[i].getTimestamp().setMilliseconds(0));
urls.push(responses[i].getEditResponseUrl());
}
for (var j = 1; j < data.length; j++) {
resultUrls.push([data[j][0]?urls[timestamps.indexOf(data[j][0].setMilliseconds(0))]:'']);
}
sheet.getRange(2, urlCol, resultUrls.length).setValues(resultUrls);
}
However, i need to expand it to run one of two different scripts, depending on if this is the initial response to a form or an edit of an existing response
//first run
if (check_if_edited(e)==false)
{
do_something();
}
//all other runs
else
{
do_something_else();
}
How do i do this?
[Edit]
I apologize: I want to do this per response, not for the first submission of the form. So the initial response runs one script, but if a user edits their response, i need to run a separate script

Answer:
Use the Properties Service to save a key-value pair after ther first run to check against in the future.
More Information:
As per the documentation:
This service allows scripts to store strings as key-value pairs scoped to one script, one user of a script, or one document in which an editor add-on is used.
So you can use it to create a variable which is bound to the script, on first run, and then change which code you want to execute based on if it exists or not. You can see it like a global-scope boolean which continues existing after the script finishes executing.
Code:
Editing your example:
function onFormSubmit() {
var sp = PropertiesService.getScriptProperties();
var user = e.response.getRespondentEmail());
//first run
if (!sp.getProperty(user)) {
do_something();
sp.setProperty(user, true);
}
//all other runs
else {
do_something_else();
}
}
Make sure to turn on the setting for collecting respondent email addresses from the ⚙️ > General > Collect email addresses checkbox in the Form UI.
You will also need to set this up as an installable trigger on form submit from the triggers page accessible from the View > Current project's triggers in the Apps Script UI.
References:
Class PropertiesService | Apps Script
Class Properties | Apps Script
Method .getProperty(key)
Method .setProperty(key, value)

Related

Google Apps Script Permissions for a function with inputs

I know this topic has bee covered before sort of, but none of it really makes sense to me. Basically I wrote a function that will generate a PDF from some API data using user inputs. It looks vaguely like this and works when I run it in the script editor.
function myfunction(InputA,InputB,InputC,InputD,InputE) {
......
var sourceSpreadsheet = SpreadsheetApp.getActive();
var parents = DriveApp.getFileById(sourceSpreadsheet.getId()).getParents();
if (parents.hasNext()) {
var folder = parents.next();
}
var response = UrlFetchApp.fetch(url, options)
var blob = response.getAs('application/pdf').setName(InputA + InputB + InputC)
var newFile = folder.createFile(blob);
return newFile
}
The problem is Google permissions. The classic: "You don't have permission to access the drive".
I have tried publishing the script as a private sheets addon and enabled it on my spreadsheet. But that didn't really do anything. And I don't really know why because I authorized the app for all the required scopes when I approved the add on. I can see it in the extensions menu but I am still getting errors when I try to call the function.
The button method of enabling permissions doesn't work for me because I need to run the code several times based on parameters defined in the Sheet. I tried simple triggers since I want the code to run weekly anyways, but found the same problem.
Can someone give me the step by step of how I'm supposed to do this.
Please don't send links to the google documentation because I have read the related pages and still don't know what I'm doing wrong.
I recommend you use an installable onEdit trigger. I have this approach and see if it works for you.
Sample Data:
Assuming url is from Input D column.
Create a column of checkboxes that will trigger the installed trigger. In my case, ticking it will create the file and unticking it will remove the file created.
Ticking rows where an input (at least 1) is missing, will cancel the creation of the file and then untick the checkbox ticked.
Drive folder:
Script:
function createFileTrigger(e) {
var spreadsheet = e.source;
var sheet = spreadsheet.getActiveSheet();
var range = e.range;
var value = e.value;
var row = range.getRow();
var col = range.getColumn();
// proceed if edited cell is Sheet1!H2:H
if(sheet.getSheetName() == 'Sheet1' && col == 8 && row > 1) {
// if checkbox is ticked
if(value == 'TRUE') {
// inputs = [Input A, Input B, Input C, Input D, Input E]
var inputs = range.offset(0, -6, 1, 5).getValues().flat();
var parents = DriveApp.getFileById(spreadsheet.getId()).getParents();
var folder = parents.next();
// set some conditions here to pre-check the inputs
// e.g. if at least 1 input is blank, cancel file creation (return)
if(inputs.filter(String).length < 5) {
// untick the checkbox ticked
range.setValue('FALSE');
// skip creation of file
return;
}
// assuming url is from Input D (removed options as not needed for presentation)
var response = UrlFetchApp.fetch(inputs[3]);
var newFileName = `${inputs[0]} ${inputs[1]} ${inputs[2]}`;
// if file is existing (which should not happen but just in case)
if(folder.getFilesByName(newFileName).hasNext()) {
// do something else that is needed to be done to avoid duplication of file
// e.g. overwrite or skip creating file
console.log(newFileName + ' is already existing in the parent folder');
}
// if not existing
else {
// create the file
var blob = response.getAs('application/pdf').setName(newFileName)
// for presenation purposes, will write the id of the created file
range.offset(0, -1).setValue(folder.createFile(blob).getId());
}
}
// if checkbox is unticked
else {
// do something else that is needed to be done
// e.g. delete the file using the id returned (using Drive Advanced Services)
var fileIdRange = range.offset(0, -1);
Drive.Files.remove(fileIdRange.getValue());
// remove file id content on the cell
fileIdRange.clearContent();
}
}
}
Ticking checkbox (folder):
Ticking checkbox (sheet):
Note:
This can still be improved, but should already be enough for your case.

onFormSubmit() not working in Google Form?

[UPDATE]
I had a look at add-ons and I am afraid this won't work. So let me take a step back and describe what I am trying to achieve.
I have a spreadsheet A, with a list of individual events. Each event is a line item in the spreadsheet. The spreadsheet is very long for one, and has many fields that I don't need to expose to event owners (different events different owners). Which means if I allow all these different people edit access to the sheet, it becomes really chaotic.
The solution I came up with is to generate unique IDs programmatically for each event, which I've done. Then for each event, I create an individual form and a pre-filled link, with pre-filled answers that is pulled from the cell values. I intend to give the pre-filled links to event owners when they need to make any updates.
The issue is now I have 100+ forms, and I don't want to have 100+ corresponding tabs set as destinations of these forms. These 100+ forms need to submit responses to one same sheet (tab). Instead I wrote a function for submitted responses to find the right event (the event unique ID is the title of the form) and updates the right cell. This is what you see below processSubmission().
I have tried to write the processSubmission() in the spreadsheet where the events are listed. If I don't set this spreadsheet as destination of these 100+ forms then the spreadsheet doesn't know there is a "submission" event. Therefore the setting the trigger onFormSubmit() in the spreadsheet doesn't work.
Then I moved onFormSubmit() -> processSubmission() and it doesn't set the trigger because as you all pointed out, it's an installable trigger.
What I did manage to to write an onOpen() -> create the onFormSubmission() trigger. That means I had to manually open 100 forms and close them to create that trigger. The triggers are created alright. But turned out for the trigger to actually run I need to manually grant permission!
When I looked at add-on triggers, it says "Add-ons can only create triggers for the file in which the add-on is used. That is, an add-on that is used in Google Doc A cannot create a trigger to monitor when Google Doc B is opened." So I think that also rules out the add-on triggers. So now I am out of ideas.
[ORIGINAL]
I made a custom function for the processing of submission responses. I use the form title as a key, and the response answers are written to the corresponding headers in the row with the right key.
My first try was something like this. But it simply didn't execute when the form was submitted:
function onFormSubmit(e){
var form = FormApp.getActiveForm();
var key = form.getTitle();
var responses = e.response;
var ss= SpreadsheetApp.openById(ss_id);
var sheet = spreadsheet.getSheetByName('Launch list');
var frozenRow = sheet.getFrozenRows();
var lastRow = sheet.getLastRow();
var lastColumn = sheet.getLastColumn();
var headers = sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
var keyCol = headers.indexOf(key_header) + 1;
var header1Col = headers.indexOf(header_1) + 1;
var header2Col = headers.indexOf(header_2) + 1;
var header3Col = headers.indexOf(header_3) + 1;
var keysRange = sheet.getRange(frozenRow+1, keyCol , lastRow - frozenRow, 1);
var allKys = keysRange.getValues();
for (i=0; i<allKys.length; i++){
var keyValue = allKys[i][0];
if (keyValue === key){
var rowNum = l + frozenRow + 1;
break;
}
else {
continue;
}
}
var dataRow = sheet.getRange(rowNum, 1, 1, lastColumn).getValues()[0];
var lookUp = {};
lookUp[item_title_1] = header1Col ;
lookUp[item_title_2] = header2Col ;
lookUp[item_title_3] = header3Col ;
var items = form.getItems();
var cnt = 0;
var changes = [];
for (i=0; i< items.length; i++){
var item = items[i];
var title = item.getTitle();
var itemResponse = responses.getResponseForItem(item);
var existingValue = dataRow[lookUp[title] -1];
if ((itemResponse.getResponse() !=='' || itemResponse.getResponse() !== undefined) && itemResponse.getResponse() != existingValue){
cnt++;
var cell = sheet.getRange(rowNum, lookUp[title], 1, 1);
cell.setValue(itemResponse.getResponse());
changes.push(title);
}
else {
continue;
}
}
Logger.log('Made ',cnt,'changes for launch ',featureID,': ',changes);
}
I also tried a slightly different approach but also didn't work:
function onFormSubmit(){
processSubmission();
}
// Processing form submission
function processSubmission() {
var form = FormApp.getActiveForm();
var key = form.getTitle();
var responses = form.getResponses()[form.getResponses().length-1];
// The rest is the same.
}
Manually running the function in the second approach proved my function processSubmission() works. Manually add a onFormSubmit() trigger via the Apps Script Dashboard is not going to be possible because I am generating hundreds of forms (one for each key) programmatically so I chose to have onFormSubmit(e) in the template and every new form is a copy of the template which should also have copies of these functions. But it just doesn't work! Any insight?
The onFormSubmit trigger is an installable trigger which means that it requires to be set up before being able to use it.
It's also important to keep in mind the following, according to the installable triggers documentation:
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.
What you can do instead is to create the trigger programmatically, something similar to this:
function createTrigger() {
ScriptApp.newTrigger('onFormSubmit')
.forForm('FORM_KEY')
.onFormSubmit()
.create();
}
Reference
Apps Script Installable Triggers;
Apps Script FormTriggerBuilder Class.

Taking data from multiple Google Forms to put into the appropriate Spreadsheet

I have multiple Google Forms set up to take in data (just numbers). That data is then grabbed using an onFormSubmit function with a manually programmed trigger. Is there a way I can use just one trigger for multiple forms and have them go to the appropriate spreadsheet?
For example, I have a form called dry storage and another called freezer (and have 6 other forms and sheets), with corresponding sheets in a spreadsheet. Is there a way that when the form is submitted, that it takes the data, and places it in the correct spreadsheet? I have it working when I manually type in the sheet name, but I would like it to be done dynamically in case more sheets and forms need to be added so a new function and trigger doesn't have to be created every time.
The forms created have already been linked to the spreadsheet. I basically want every linked form submitted to get the formID of the form it was submitted, and be able to match it to the corresponding sheet name.
In a perfect world, the spreadsheet is determined by the form it is submitted on by name. Is it possible to do this dynamically?
I would like to not have 8 triggers with 8 functions that all do the exact same thing, with the only difference is the formID and corresponding sheet being passed.
The onSubmit function does exactly what I want it to do for one form and one spreadsheet, but I would like it to be able to take in any form and map it to the correct sheet.
The onFormSubmit function was me attempting to use events to generalize it, but with no avail, as I am unfamilar with how events work. I've searched high and low for an example that matches what I am trying to do on the Google website, and StackOverflow. Any examples or points on how events work would be beneficial as well.
Thank you!
function onFormSubmit(e)
{
var res = e.values // get the data from the form
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dry Storage"); // manually get the sheet it should be going to, I'd like this to be dynamic
for (var a = 0; a < res.length; a++)
{
sheet.getRange(7,(a+1)).setValue(e.values[a]) // set the form values in the sheet
}
function trigger()
{
var form = FormApp.openById(DryStorageFormID) // formID for dry storage. is there a way that this ID can be grabbed dynaically from the submitted form?
ScriptApp.newTrigger('onSubmit')
.forForm(form)
.onFormSubmit()
.create();
}
function onSubmit()
{
var ss = SpreadsheetApp.getActiveSpreadsheet(); // get active spreadsheet
var targetSheet = ss.getSheetByName("Dry Storage"); // data should be sent to this sheet. I would like this info to be somehow passed to the function instead of have it hard-coded.
var form = FormApp.openById(DryStorageFormID); // DryFormID
try{
var formResponses = form.getResponses();
}
catch(e)
{
console.error("No Data!");
}
var itemResponses = formResponses[formResponses.length - 1].getItemResponses();
for (var j = 0; j < itemResponses.length; j++)
{
var itemResponse = itemResponses[j];
var offset = j + 2
targetSheet.getRange(offset, (FR.order +1)).setValue(itemResponse.getResponse())
}
}
The easier way to proceed, instead of having one spreadsheets for each form, is to have one spreadsheet that receive responses from multiple forms.
Besides the above, you could send the received responses to another spreadsheet using the same on form submit trigger or by other means (i.e. IMPORTRANGE).
To get the sheet receiving the form response you could use the following code line (assuming that e is the spreadsheet on form submit object):
var sheet = e.range.getSheet();
then you could use sheet.getName() to get the sheet name or whatever else you need of that sheet.
To get the "name" of the form that triggered the on form submit trigger you could use:
var url = sheet.getFormUrl();
var form = FormApp.openFormByUrl(url);
var title = form.getTitle();
Related
Identifying Form destination (Spreadsheet AND SHEET)
How to get a form linked to a sheet using google app script?

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

Code not applying a timely sheet edit

I have code in a spreadsheet with a trigger to run it on form submit. The code is supposed to first create a link to allow for editing the submitted form data. It has worked fine in older sheets, but in my latest iteration, it is not saving the link before proceeding.
This is the beginning of the code which is the function listed in the trigger:
function sendRegEmails(e) {
var emailSubject = templateSheet.getRange("B3").getValue();
var emailHTMLTemplate = templateSheet.getRange("B4").getValue();
var emailWSAddInToHTMLTemplate = templateSheet.getRange("B9").getValue();
var emailWSReqFormLinkHTMLTemplate = templateSheet.getRange("B10").getValue();
//Create and save the URL to allow the respondent to edit their registration
assignEditUrls(REGISTRATION_FORM_ID, REGISTRATION_SHEETNAME, REGISTRATION_LINK_COL);
Utilities.sleep(5000);// pause in the loop for 5000 milliseconds or 5 seconds to make sure the URL is in the worksheet
// Create one JavaScript object per row of data.
var objects = getRowsData(mainsheet);
// For every row object, create a personalized email from a template and send
// it to the appropriate person.
for (var i = 0; i < objects.length; ++i) {...(continues)
This code is in a different .gs file in the same project:
/**-----------------------------------------------------------------------------------
|
| Begin section to create link to editable form
|
------------------------------------------------------------------------------------*/
function assignEditUrls(PassedForm_ID, SheetName, urlCol) {
var form = FormApp.openById(PassedForm_ID);
var sheet = SpreadsheetApp.openById(REGISTRATION_SHEET).getSheetByName(SheetName);
var data = sheet.getDataRange().getValues();
var responses = form.getResponses();
var timestamps = [], urls = [], resultUrls = [];
for (var i = 0; i < responses.length; i++) {
timestamps.push(responses[i].getTimestamp().setMilliseconds(0));
urls.push(responses[i].getEditResponseUrl());
}
for (var j = 1; j < data.length; j++) {
resultUrls.push([data[j][0]?urls[timestamps.indexOf(data[j][0].setMilliseconds(0))]:'']);
}
sheet.getRange(2, urlCol + 1, resultUrls.length).setValues(resultUrls);
}
When the email is sent the spreadsheet does not contain the URL. Has there been a change which would cause the assignEditURLs function to not save the URLs in the spreadsheet until after the other script is complete? Has something been added that I need to include in the code to get this to be added? I would like the email to go out within a couple minutes of the form submit.
As I said, this has worked in other spreadsheets. The only changes made to code has been to use the correct columns and files. The data ends up in the sheet, but not until after the email is sent.
REGISTRATION_FORM_ID = the ID of the Form file
REGISTRATION_SHEETNAME = the name of the sheet to receive the data
REGISTRATION_LINK_COL = the column number in REGISTRATION_SHEETNAME to place the data
REGISTRATION_SHEET = the ID of the Sheet file to receive the data
Regards,
Karl
On some scripts I maintain I've noticed with the transition to new sheets, the sheet ID changes. If you haven't already, try updating the REGISTRATION_SHEET variable with the new id.