onFormSubmit() not working in Google Form? - google-apps-script

[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.

Related

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?

How to update a single event without duplicating the rest of the events

I am currently using app script to sync my google sheet to google calendar. The process is quite simple, my script only takes a date and title from the spreadsheet and creates an all day event on that date with that title.
The problem I am facing is that if I accidentally key in the wrong date or I have to update one of the dates inside the spreadsheet, on running the scheduleShifts function again, all the events are created again which results in many duplicate events that I did not intend to be there. I'm trying to find a solution that helps either updates the title of the event or deletes the event and create a new one in the case where the date that is in the spreadsheet is wrong.
It also isn't very efficient to update the data in the spreadsheet and then update the calendar because in the event where quite a few dates or titles have to be changed, it would take quite a bit of time to change them all in the calendar. It would also be very troublesome to delete the current calendar, create a new one, copy that id into the spreadsheet and then update everything again.
This is what my current code looks like:
function scheduleShifts()
{
/*Identify calendar*/
var spreadsheet = SpreadsheetApp.getActiveSheet();
var calendarId = spreadsheet.getRange("C1").getValue();
var eventCal = CalendarApp.getCalendarById(calendarId);
/*Import data from the spreadsheet*/
var signups = spreadsheet.getRange("C4:F73").getValues();
/*Create events*/
for (x=0; x<signups.length; x++)
{
var shift = signups[x];
var title = shift[0];
var date = shift[3];
eventCal.createAllDayEvent(title, date);
}
}
/*Make the script shareable for others to use*/
function onOpen()
{
var ui = SpreadsheetApp.getUi();
ui.createMenu('Sync to Calendar')
.addItem('Schedule shifts', 'scheduleShifts')
.addToUi();
}
I have tried to avoid duplicating events by retrieving all the events with Advanced Calendar Service, pushing their titles into an array and then verifying with IndexOf. However, I am unsure if this method will work if the title stays the same while there is an update in the date of that event.
The code that I referenced from to do this:
var existingEvents=Calendar.Events.list(calendarId);
var eventArray=[];
existingEvents.items.forEach(function(e){eventArray.push(e.summary)});
for (x=0; x<signups.length; x++) {
var shift = signups [x];
var startTime = shift[0];
var endTime = shift[1];
var inspector = shift[2];
if(eventArray.indexOf(inspector)==-1){
eventCal.createEvent(inspector, startTime, endTime);
}else{
Logger.log('event exists already');
}
}
If anyone needs more info feel free to ask in the comments, your help would be greatly appreciated.
You have developed a script to create events using data from a sheet.
You want to be able to update previously created events while avoiding creating duplicates.
Step 1. Avoid creating duplicates:
In order to avoid creating duplicates, you could make the script write the corresponding eventId when each event has been created. This way, next time the script runs, it can check whether the eventId is populated (in which case the event already exists), and only create the event if it doesn't exist. It could be something like this (in this sample, the eventIds are written to column G):
function scheduleShifts() {
const spreadsheet = SpreadsheetApp.getActiveSheet();
const calendarId = spreadsheet.getRange("C1").getValue();
const eventCal = CalendarApp.getCalendarById(calendarId);
const signups = spreadsheet.getRange("C4:G7").getValues();
for (let x = 0; x < signups.length; x++) {
const shift = signups[x];
const title = shift[0];
const date = shift[3];
const eventId = shift[4];
if (eventId == "") { // Check the event doesn't exist
const newEvent = eventCal.createAllDayEvent(title, date);
const newEventId = newEvent.getId();
spreadsheet.getRange(4 + x, 7).setValue(newEventId); // Write new eventId to col G
}
}
}
Step 2. Update events:
Regarding the update process, I'd suggest you to install an onEdit trigger, either manually or programmatically. This this action requires authorization, a simple onEdit trigger would not work here (see Restrictions).
This way, you can use the event object to only update the event corresponding to the row that was edited, thus avoiding having to update all events every time, which would make the process very inefficient. The update process itself would consist on calling setTitle and setAllDayDate.
The function fired by the onEdit trigger could be something like this:
function updateEvent(e) {
var editedRow = e.range.getRow();
var editedData = e.source.getActiveSheet().getRange(editedRow, 3, 1, 5).getValues()[0];
var eventId = editedData[4];
try {
var event = CalendarApp.getEventById(eventId);
event.setTitle(editedData[0]);
event.setAllDayDate(editedData[3]);
} catch(err) {
console.log("There was a problem updating the event. This event might not exist yet.");
}
}
Notes:
You may not want to manually set the range (C4:G73) if the number of events might vary. You can use methods like getLastRow() no make this range dynamic, based on the spreadsheet content.
The eventId that are used here, corresponding to Class CalendarEvent, are not identical to the API Event resource. Take this into account in case you use the Advanced Service.

Google Trigger for Edited Form Entry

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)

Programmatically created trigger is only for current file, despite giving key of other file

I'm trying to programmatically create a trigger for a Google Spreadsheet when a form is submitted with code from another Spreadsheet, but it won't work. Here is my code:
function createOnFormSubmitTrigger()
{
//Id of "Spreadsheet 1"
var ssId = "18bq-67nP4y7F9Hp4jzpbKPCyAsR6hglgcfmxCi_zj14";
ScriptApp.newTrigger("formInput").forSpreadsheet(ssId).onFormSubmit().create();
}
If I put that method into "Spreadsheet 1" and run it, it works fine and creates the script in Spreadsheet 1 as intended. However, if I put that method into say "Spreadsheet 2" and run it, it creates the trigger in Spreadsheet 2 instead of Spreadsheet 1, which is not as intended. What am I doing wrong?
Here is the code for formInput for the original script and the dummy script I made for testing:
Original:
function formInput()
{
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var inSheet = spreadSheet.getSheetByName("Form Responses");
var data = inSheet.getSheetValues(2,2,1,7);
var minutes = data[0][0];
var seconds = data[0][1];
var numMissed = data[0][2];
var sgIncorrect = data[0][3];
var sfMissed = data[0][4];
var year = data[0][5];
var testLevel = data[0][6];
var date = new Date();
var outSheet = spreadSheet.getSheetByName(testLevel);
var testLevelExists = (outSheet != null);
if(testLevelExists)
{
outSheet.insertRowBefore(2);
outSheet.getRange(2,1).setValue(date.getMonth()+1+ "/" + date.getDate());
outSheet.getRange(2,2).setValue(year);
var secStr = ("0" + seconds);
outSheet.getRange(2,3).setValue(minutes + ":" + secStr.substring(secStr.length-2));
outSheet.getRange(2,4).setValue(numMissed);
outSheet.getRange(2,5).setValue(sgIncorrect);
outSheet.getRange(2,6).setValue(sfMissed);
outSheet.getRange(2,7).setValue(350-7*(sgIncorrect+numMissed)-2*(sfMissed));
shiftBox(outSheet);
setFormulas(outSheet);
updateAverageTime(outSheet);
inSheet.deleteRow(2);
copyToMaster(spreadSheet,outSheet);
}
}
Dummy:
function formInput()
{
var hi = "hello";
}
I'd like to thank #JSmith for all his help first of all. I seemed to have misunderstood the creation of new triggers using the ScriptApp API. When the script is created, it links the trigger to actions performed on the target sheet, but the code must be in the original sheet which created the trigger. The trigger is also only displayed on the sheet which created the trigger.
As seen together,
try all your functions from google's IDE once.
Also it seems a trigger created from a certain spreadsheet is viewable from the trigger menu of the original spreadsheet script even if this trigger is linked to another spreadsheet.

Automating Sheet Creation in newly created Google Sheet+Form

I am trying to create a script that adds a Google Form response sheet to a newly created Google Sheet, then creates subsequent Sheets in the same Spreadsheet. The sheets are named upon creation. Here is my code thus far:
var statusSheet, form, ss = SpreadsheetApp.openById("SPREADSHEET_ID")
if (ss.getFormUrl() == null){
form = FormApp.create("NEWFORM")
.setCollectEmail(true)
.setProgressBar(false)
.setRequireLogin(true)
.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId())
Utilities.sleep(3000)
}
else{form = FormApp.openByUrl(ss.getFormUrl())}
if (ss.getSheets()[1].getRange("A1").getValue() != 'Status'){
ss.getSheets()[1].setName("Status")
.getRange("A1:C8")
.setValues([["Status",'',"Staff"],["Staff",'',"Student"],["Students",,"building"],["Penalty Box",'',"scope"],["Current Offenders",'',"formCreated"],["Audit Log",'',"formPopulated"],['','',"TimeTrigger"],["Form ID",'',"EditTrigger"]])
Utilities.sleep(3000)
}
statusSheet = ss.getSheetByName("Status")
ss.setActiveSheet(ss.getSheets()[(ss.getSheets().length)-1])
while (ss.getSheets().length < 7){
var newSheet = ss.insertSheet()
var range = statusSheet.getRange(newSheet.getIndex(),2, 1, 1)
range.setValue(newSheet.getSheetId())
newSheet.setName(statusSheet.getRange(newSheet.getIndex(),1, 1, 1).getValue())
Logger.log(newSheet.getName())
}
var staffSheet = ss.getSheetByName("Staff")
var studentsSheet = ss.getSheetByName("Students")
var PenaltyOUSheet = ss.getSheetByName("OrgUnits")
var currentBoxUsersSheet = ss.getSheetByName("Current Users")
var auditLogSheet = ss.getSheetByName("Audit Log")
It seems that all of the sheets are created, except "Staff". This is the case whether the form is created and/or the values are populated either before executing the function or during the execution. To make matters worse, the Logger within the for Loop isn't recording errors.
I have tried encasing the whole function in a try/catch statement, but there are no indications what the error might be other than the reference to staffSheet fails, because no sheets are named "Staff".
Moving the copied range down a row fixed the issue. I'm not positive why it was skipping the Staff sheet rather than the Audit Log, but I will figure that out after I get everything else fixed.