How to access Spreadsheet from a Google Forms submit trigger function - javascript

I have a function in Google Forms script to add a Unique ID to the row being added to the associated Sheet. This function is triggered on submission of the form.
Here's one version of the script I've been playing around with:-
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value.
var id = sheet.getRange("Z4").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(id);
sheet.getRange("Z4").setValue("Z4"+1)
}
}
On debugging I get the message:-
TypeError: Cannot call method "getActiveCell" of null. (line 5, file "CreateID")
I've tried cropping the code right down to just a simple setValue, and I get the same issue - "cannot call method...", with pretty much every line except the getActiveSheet.
The trigger of the function works ok, as I get notifications to say that the function itself had failed to execute successfully. I've looked online, tried a few things, but can't find a solution as yet.
So what I'm really after is the correct method of accessing the spreadsheet that the form is posting to. If SpreadsheetApp.getActiveSheet() isn't the right method, what is?
Totally new to this script, last programmed in PLI(!), sorry. Any pointers to existing solutions or other, would be appreciated.

When a form is submitted, the trigger function has no active spreadsheet, nor will it have an active cell. (It's not associated with a Sheets UI, so those concepts are meaningless.)
However, the event parameter, e, will provide information about the row that has been added by the Form. See Google Sheets events:
e.range contains a Range object covering the cells that have been filled by the form submission that triggered your function. You can backtrack from there to get the sheet.
sheet = e.range.getSheet();
You function becomes something like this:
function onFormSubmit(e) {
// Get the active sheet
var sheet = e.range.getSheet();
// Get the active row
var row = e.range.getRowIndex();
// Get the next ID value.
var id = sheet.getRange("Z4").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(id);
sheet.getRange("Z4").setValue("Z4"+1)
}
}
Now, you have other problems to deal with.
sheet.getRange(row, 1).getValue() is not necessary; that value has just been handed to you as e.values[0].
However, on a form submission, the first column contains a Timestamp, so it won't ever be empty. If your "ID" value is the first question on the form, then it's actually in column 2, or e.values[1].
The cell Z4 will likely move on you, as form submissions insert new rows into the sheet. It would be better to pull that value from a different sheet - or better yet use the Properties Service to manage it.
To make use of the event, you'll need to simulate it for testing. Read over How can I test a trigger function in GAS?.

You need to check whether getActiveSheet() succeeded.
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
if (!sheet) { // No active sheet, nothing to do
return;
}
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value.
var id = sheet.getRange("Z4").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(id);
sheet.getRange("Z4").setValue("Z4"+1)
}
}

Related

Run function when new row added to specific sheet [duplicate]

i have 3 sheets my spreadsheet. Names is sheet1, sheet2, sheet3. And i have a function "onChange()". i install onChange function at triggers. i want run onChange function if only when add new row at sheet2 . But not working properly. if i write sheet1 or sheet2 or sheet3 anything always run my onChange function.
function onChange(){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet2");
var sheet2lr = ss.getRange(ss.getLastRow(),1,1,2).getValues();
var sheet1lr = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet1");
sheet1lr.getRange(getLastRow(),1,1,4).setValues(sheet2lr)}
I believe your goal as follows.
You want to run the script of onChange() when new row is inserted to "sheet2".
Your function of onChange() has already been installed as the OnChange installable trigger.
In this case, I thought that changeType and source of the event object can be used for achieving your goal. When this is reflected to your script, it becomes as follows.
Modified script:
function onChange(e) {
if (e.changeType != "INSERT_ROW" || e.source.getActiveSheet().getSheetName() != "sheet2") return; // Added
// do something
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet2"); // Or e.source.getSheetByName("sheet2");
var sheet2lr = ss.getRange(ss.getLastRow(), 1, 1, 2).getValues();
var sheet1lr = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet1"); // Or e.source.getSheetByName("sheet1");
sheet1lr.getRange(getLastRow(), 1, 1, 4).setValues(sheet2lr)
}
In this modified script, when new row is inserted to "sheet2", the script below the if statement is run.
When you want to run this script, please insert new row to the sheet of "Sheet2".
Note:
If above modified didn't work, please reinstall the OnChange trigger to the function onChange.
For your script, I think that you can also the event object of e.source instead of SpreadsheetApp.getActiveSpreadsheet().
Reference:
Event Objects
Added:
About your following comment,
thanks Tanaike for help me. if the user adds a row to sheet2, the onchange function works(only change equlas oparator != change ==).actually another application is adding a row to sheet2. when if i use 1 control e.source.getActiveSheet().getSheetName() == "sheet2") working but if i use second control not fire onChange e.changeType != "INSERT_ROW".
In the current stage, in your situation, OnChange trigger can be used when the row is manually inserted and the row is inserted with Sheets API which is not Google Spreadsheet service (SpreadsheetApp). So from your comment, in your situation, I thought that the row is inserted with SpreadsheetApp. If my understanding is correct, I would like to propose the following 2 patterns.
Pattern 1:
In this pattern, your current script of onChange() is called from the function of actually another application is adding a row to sheet2. When your script of actually another application is adding a row to sheet2 is including in the same Google Apps Script project of onChange() and it supposes that the function name is sample(), you can modify as follows.
function sample() {
// do something of `actually another application is adding a row to sheet2`
onChange();
}
function onChange(){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet2");
var sheet2lr = ss.getRange(ss.getLastRow(),1,1,2).getValues();
var sheet1lr = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet1");
sheet1lr.getRange(getLastRow(),1,1,4).setValues(sheet2lr)
}
Pattern 2:
In this pattern, the row is inserted with Sheets API in your script of actually another application is adding a row to sheet2. In this case, you can convert the script for inserting row using InsertDimensionRequest of the batchUpdate method. Ref When this script is run, my proposed script can be used. Because when new row is inserted with Sheets API, the OnChange trigger is fired. In this case, I cannot understand about your script of actually another application is adding a row to sheet2. By this, I just propose the method for achieving your goal using Sheets API.

Is there a way to define a changeType for an onChange event trigger in Google Apps Script? [duplicate]

Looking to get the name of the newly added sheet (manually added) using an installable trigger.
ChangeType with INSERT_GRID worked fine for me (tested by outputting some random value when sheet gets added) but when I try getting the name of the newly added sheet, it instead gives me the value of the FIRST sheet's name in the document.
Any alternatives? Have heard that this might be a previous bug in App Script.
function log(e)
{
if (e.changeType === "INSERT_GRID") {
var news = e.source.getActiveSheet().getName(); //fails to get correct value
e.source.getSheetByName("Client").getRange(1, 1).setValue(news);
}}
I believe your goal as follows.
You want to detect the sheet name of added sheet using the OnChange event trigger.
In your current issue, you cannot retrieve the sheet name of added sheet. When this is the bug or specification, you want to know about the alternatives for achieving your goal.
For this, how about this answer?
Issue and workaround:
Unfortunately, in the current stage, it seems that the event object of OnChange event trigger has not information about the added sheet. By this, when your script is used, the sheet name of the 1st tab is retrieved. I'm not sure whether this is the bug or the current specification.
So in order to achieve your goal, I would like to propose the following workaround. The flow of this workaround is as follows.
At first, the initial sheet names are saved to PropertiesService.
When the sheet is added, the aded sheet is retrieved by comparing the current sheets and initial sheets retrieved from the PropertiesService.
Sample script:
In order to use this script, please close the Google Spreadsheet and open it again. By this, onOpen is run and the initial sheet names are saved to the PropertiesService. Then, when the sheet is added, the added sheet is retrieved and the sheet name of the added sheet is put to the cell.
function saveCurrentSheets(prop, spreadsheet) {
const sheets = spreadsheet.getSheets().reduce((o, s) => Object.assign(o, {[s.getSheetName()]: true}), {});
prop.setProperty("sheets", JSON.stringify(sheets));
}
function onOpen(e) {
saveCurrentSheets(PropertiesService.getScriptProperties(), e.source);
}
function log(e) {
if (e.changeType === "INSERT_GRID") {
const prop = PropertiesService.getScriptProperties();
const oldSheets = prop.getProperty("sheets");
if (!oldSheets) {
saveCurrentSheets(prop, e.source);
return;
}
const oldSheetsObj = JSON.parse(prop.getProperty("sheets"));
const addedSheet = e.source.getSheets().filter(s => !oldSheetsObj[s.getSheetName()])[0];
const news = addedSheet.getSheetName();
e.source.getSheetByName("Client").getRange(1, 1).setValue(news);
saveCurrentSheets(prop, e.source);
} else if (e.changeType === "REMOVE_GRID") {
saveCurrentSheets(PropertiesService.getScriptProperties(), e.source);
}
}
In this script, please install the OnChange event trigger to the function of log.
In this script, when the existing sheet is deleted, the values of PropertiesService are updated by the current sheet names.
Note:
When there are a lot of sheets in the Google Spreadsheet, I think that the initial sheet names are required to be a file instead of the PropertiesService, because of "Properties value size is 9kB / val." and "Properties total storage is 500kB / property store.". Please be careful this.
References:
Event object of OnChange
Class Properties
Quotas for Google Services
That's the normal behavior. It always returns e.source.getSheets()[0]; Which is the same thing if you get the active sheet when you openById();

Google Apps Script is creating calendar entry twice

I have mashed together a Google Apps script to create an event in the calendar based on values in my Google sheet. The script should work in the following conditions:
Whenever a new row is added (achived by a trigger - the 1st function)
Only apply to the last row on the spreadsheet
Only create an event if the "id" cell (28) is empty
It should then create the calendar event and populate cell 28 with the event ID.
The new rows are created when a client fills out a form on my website. I'm using Ninja Forms in WordPress that has a Google Sheets plugin. So the filled out form is automatically added to the sheet, then this function fires.
Everything is almost working. When I test manually (eg I delete the id cell in the last row or I create a new row or copy an existing row) it works perfectly. The event is created only if there is no event ID in cell 28 already, and it successfully fills out cell 28 with the ID. Great!
However, when the row is created using the WordPress form I get two calendar events. IE - it's like the function runs twice. Each event is identical and both are created at the same time.
I'm guessing this has something to do with how the form integrates with the sheet. It is somehow triggering my function twice. I have tried using Utilities.sleep at different points in the function with different values to see if maybe waiting between strps helps, but to no avail.
Can anyone think of a way I can stop this from occurring? Is there some kind of check I might be able to build into my function? Or have I missed something obvious? I would really appreciate any suggestions.
Here is my code:
function initializeTrigger(){ // run once only to create the trigger
var sheet = SpreadsheetApp.getActive();
ScriptApp.newTrigger("NewCalEvent")
.forSpreadsheet(sheet)
.onChange()
.create();
}
function NewCalEvent() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Bookings2');
var row = sheet.getLastRow();
var calId = "xxxyyy#group.calendar.google.com";
var cal = CalendarApp.getCalendarById(calId);
var date = sheet.getRange(row, 1).getValue();
var title = sheet.getRange(row, 26).getValue();
var tz = sheet.getRange(row, 23).getValue();
var tstart = new Date(sheet.getRange(row, 32).getValue());
var tstop = new Date(sheet.getRange(row, 33).getValue());
var loc = sheet.getRange(row, 2).getValue();
var desc = sheet.getRange(row, 27).getValue();
var guests = sheet.getRange(row, 29).getValue();
var id = sheet.getRange(row, 28).getValue();
if (id == ""){
var newEvent = cal.createEvent(title, tstart, tstop, {description:desc,location:loc,guests:guests,sendInvites:true}).getId();
sheet.getRange(row, 28).setValue(newEvent)
}
}
Possible Solution:
Add a check to see if the calendar event has already been created for this time.
More Information:
As it is not clear exactly where this trigger duplication is coming from, a way to circumvent the event being created twice is to search the calendar for the specified time frame for events which have the description you intend to enter, and then wrap the event creation code inside the check so it only runs if the event doesn't yet exist.
Code Snippet:
// ...
var id = sheet.getRange(row, 28).getValue();
var events = cal.getEvents(tstart, tstop, {search: desc})
.map(function(e) {
try {
return e.getDescription();
}
catch {
return "";
}
});
if (!events.includes(desc)) {
if (id == ""){
var newEvent = cal.createEvent(title, tstart, tstop,
{description:desc,location:loc,guests:guests,sendInvites:true}).getId();
sheet.getRange(row, 28).setValue(newEvent)
}
}
This way, if the script is run twice, then as the event already exists the second time, it will not be created again.
NB: This checks for all Calendar event exists within the time frame tstart to tstop that have a description of desc. If you happen to have other events within this time frame with the same description, this script may not behave as expected.
References:
Class CalendarApp - getEvents(startTime, endTime, options) | Apps Script | Google Developers

Cannot getname() newly added google sheet using changeType INSERT_GRID trigger

Looking to get the name of the newly added sheet (manually added) using an installable trigger.
ChangeType with INSERT_GRID worked fine for me (tested by outputting some random value when sheet gets added) but when I try getting the name of the newly added sheet, it instead gives me the value of the FIRST sheet's name in the document.
Any alternatives? Have heard that this might be a previous bug in App Script.
function log(e)
{
if (e.changeType === "INSERT_GRID") {
var news = e.source.getActiveSheet().getName(); //fails to get correct value
e.source.getSheetByName("Client").getRange(1, 1).setValue(news);
}}
I believe your goal as follows.
You want to detect the sheet name of added sheet using the OnChange event trigger.
In your current issue, you cannot retrieve the sheet name of added sheet. When this is the bug or specification, you want to know about the alternatives for achieving your goal.
For this, how about this answer?
Issue and workaround:
Unfortunately, in the current stage, it seems that the event object of OnChange event trigger has not information about the added sheet. By this, when your script is used, the sheet name of the 1st tab is retrieved. I'm not sure whether this is the bug or the current specification.
So in order to achieve your goal, I would like to propose the following workaround. The flow of this workaround is as follows.
At first, the initial sheet names are saved to PropertiesService.
When the sheet is added, the aded sheet is retrieved by comparing the current sheets and initial sheets retrieved from the PropertiesService.
Sample script:
In order to use this script, please close the Google Spreadsheet and open it again. By this, onOpen is run and the initial sheet names are saved to the PropertiesService. Then, when the sheet is added, the added sheet is retrieved and the sheet name of the added sheet is put to the cell.
function saveCurrentSheets(prop, spreadsheet) {
const sheets = spreadsheet.getSheets().reduce((o, s) => Object.assign(o, {[s.getSheetName()]: true}), {});
prop.setProperty("sheets", JSON.stringify(sheets));
}
function onOpen(e) {
saveCurrentSheets(PropertiesService.getScriptProperties(), e.source);
}
function log(e) {
if (e.changeType === "INSERT_GRID") {
const prop = PropertiesService.getScriptProperties();
const oldSheets = prop.getProperty("sheets");
if (!oldSheets) {
saveCurrentSheets(prop, e.source);
return;
}
const oldSheetsObj = JSON.parse(prop.getProperty("sheets"));
const addedSheet = e.source.getSheets().filter(s => !oldSheetsObj[s.getSheetName()])[0];
const news = addedSheet.getSheetName();
e.source.getSheetByName("Client").getRange(1, 1).setValue(news);
saveCurrentSheets(prop, e.source);
} else if (e.changeType === "REMOVE_GRID") {
saveCurrentSheets(PropertiesService.getScriptProperties(), e.source);
}
}
In this script, please install the OnChange event trigger to the function of log.
In this script, when the existing sheet is deleted, the values of PropertiesService are updated by the current sheet names.
Note:
When there are a lot of sheets in the Google Spreadsheet, I think that the initial sheet names are required to be a file instead of the PropertiesService, because of "Properties value size is 9kB / val." and "Properties total storage is 500kB / property store.". Please be careful this.
References:
Event object of OnChange
Class Properties
Quotas for Google Services
That's the normal behavior. It always returns e.source.getSheets()[0]; Which is the same thing if you get the active sheet when you openById();

How to pull a specific row from one spreadsheet to another, update it and then replace it on the original sheet

So I have two separate sheets, one with a data list full of unique names, their location and their current status. The second would have an input to type someones name and it would pull up the row that contains their current location/status(first function), from there I could change the status and run the second function that updates the first sheet with the new status. I have been googling all day trying to cobble together something that would work and I think I am almost there on the first function but I am stuck and any help to point me in the right direction would be greatly appreciated.
Ideally the first function would trigger upon typing the name in and the second would trigger upon changing the status but I can find a manual workaround if need be.
function dataPull() {
var data = SpreadsheetApp.openById('Spreadsheet_ID'); //replace with Data spreadsheet ID
var filteredRows = data.filter(function (data) {
var employee = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange('b2').getValue();
if (data[1] === employee) {
return data
}
});
SpreadsheetApp.getActiveSheet().getRange('A2:D').setValue(filter)
}
Here are some screenshots that will hopefully better explain what I am looking to accomplish.
Sample data
Type someones name in Input sheet
Pull up current status w/ function one
Update Status and run function two
Sample data updates to reflect change in status
Edit Dialog in a Sheet
There is two sections to this function. The top section triggers off of edits to the Edit sheet in column 2 and row 2 which is a dropdown that contains all of the name in the Data Sheet. An edit here cause the top section to run which reads the the data on the Data sheet and file a matching name when it finds it that line is loaded into columns 1 through 4 and row 2 of the Edit sheet. Then you can edit the status and click the checkbox to save it back to the Data sheet. The lower portion then runs which reset's the checkbox, clears the Edit range and loads the edits back into the Data Sheet.
Please take a look at the animation to see the process in action.
Just to let you know upfront, you can run this function from the script editor you must put it into you sheet and name your sheets properly. You must also set up the data validations for the two drop downs. If you try to run this function from the script editor you will get the Error: Cannot find method getsheet() of undefined
function onEdit(e) {
e.source.toast('Entry');
var sh=e.range.getSheet();
if(sh.getName()=='Edit' & e.range.columnStart==2 && e.range.rowStart==2) {
e.source.toast('flag1');
var found=false;
var dsh=e.source.getSheetByName('Data');
var vA=dsh.getRange(2,1,dsh.getLastRow()-1,dsh.getLastColumn()).getValues();
for(var i=0;i<vA.length;i++) {
if(vA[i][1]==e.range.getValue()) {
sh.getRange(2,1,1,4).setValues([vA[i]]);
found=true;
e.source.toast('Value Found at i = ' + i + 'value = ' + vA[i][1],'',-1);
break;
}
}
if(!found) {
e.source.toast('No Value found = ' + e.value);
}
Logger.log(vA);
}
if(sh.getName()=='Edit' & e.range.columnStart==5 && e.range.rowStart==2) {
e.range.setValue("FALSE");
dsh=e.source.getSheetByName('Data');
dsh.getRange(Number(e.range.offset(0,-4).getValue()+1),4).setValue(e.range.offset(0,-1).getValue());
sh.getRange(2,1,1,4).clearContent();
}
}
Data Sheet:
Edit Sheet:
Animation:
You can't use the built-in onEdit(e) trigger; as it behaves in the same manner as a custom function would eg.(Restricted to the sheet it's bound to).
You can install a trigger in your bound script, and have it execute your un-restricted function.
function importData(e){
// Get the cell where the user picked its name
var range = e.range;
// The Id of your data source sheet
var dataSheetId = 'XXXXXXXXXXXXX';
// Get all values from source sheet as an array
var data_values = SpreadsheetApp.openById(dataSheetId).getDataRange().getDisplayValues();
// Return the row with data for matching employee
var filteredRow = data_values.filter(check)[0];
// Input location data in input sheet
SpreadsheetApp.getActiveSheet().getRange(range.getRow(),range.getColumn()+1).setValue(filteredRow[2]);
}
function check(row) {
// Get the name that the user picked
var employee_name = SpreadsheetApp.getActiveSheet().getActiveCell().getValue();
// Check if it matches and return the row
if (row[1].toLowerCase() === employee_name.toLowerCase()) {
return row
}
}
You can install the trigger from within your Apps Script by following Edit/Current project's triggers/Add trigger.
As the event source for the trigger select: "From spreadsheet"; and for function to run select "importData" (or the name of your function).

Categories