google-apps-script: No "source" field on event trigger - google-apps-script

I've set up a trigger for submission of a Google form to run one of my Google scripts:
ScriptApp.newTrigger('onFormSubmitted')
.forForm(form).onFormSubmit().create();
My onFormSubmitted function gets triggered, but the event argument supplied to the function has no source attribute. It has 3 of the 4 fields that the documentation says it should have.
So how should I get a reference to the form that triggered this event?
TMI
function onFormSubmitted(data, arg) {
log(8, data['triggerUid'], -1) // => 1874727473075378640
log(9, data['authMode'], -1) // => FULL
log(10, data['response'], -1) // => FormResponse
log(11, data['source'], -1) // => undefined
}

As mentioned in the documentation for the event object, if the script is not bound, then there is no associated source property. You can overcome this by storing the form id in properties using the trigger uid. Then in your form submit code, use the trigger uid to obtain the proper Form.
function getFormId_(triggerId) {
const store = PropertiesService.getScriptProperties();
var formId = store.getProperty(triggerId);
if (!formId) console.warn("No form ID found for trigger ID '" + triggerId);
return formId;
}
function createSubmitTrigger_(form, functionName) {
const formId = form.getId();
const trigger = ScriptApp.newTrigger(functionName).forForm(form)
.onFormSubmit()
.create();
const store = PropertiesService.getScriptProperties();
store.setProperty(trigger.getUniqueId(), formId);
}
function myFormSubmit(e) {
const form = FormApp.openById(getFormId_(e.triggerUid));
...
}
It's possible this approach (store & retrieve) can be backwards-applied, though this will depend on how the triggers have previously been configured - you cannot programmatically (or otherwise) interact with others' triggers.
function storeAll() { // Run once (more is ok too, will just overwrite existing keys).
function cb(acc, t, i, allVals) { // callback for Array#reduce
acc[t.getUniqueId()] = t.getTriggerSourceId();
return acc;
}
const newProps = ScriptApp.getProjectTriggers().reduce(cb, {});
PropertiesService.getScriptProperties().setProperties(newProps);
}
References
Trigger class
getProjectTriggers is only for current user
Array#reduce

Related

Two App scripts running on Forms and Sheets, need to connect them both

I have an onboarding form that puts all the responses in a google sheet which has an app script running to add user to google admin and different groups by taking in values from the last row of the sheet. That works fine, it's just that I have to run the script every time the form is filled so I want to create a form trigger.
It made sense to create a form submit trigger on the app script attached to the google form and I added the library and script id of the other appscipt and pulled in a method from there like such
// Create a form submit installable trigger
// using Apps Script.
function createFormSubmitTrigger() {
// Get the form object.
var form = FormApp.getActiveForm();
// Since we know this project should only have a single trigger
// we'll simply check if there are more than 0 triggers. If yes,
// we'll assume this function was already run so we won't create
// a trigger.
var currentTriggers = ScriptApp.getProjectTriggers();
if(currentTriggers.length > 0)
return;
// Create a trigger that will run the onFormSubmit function
// whenever the form is submitted.
ScriptApp.newTrigger("onFormSubmit").forForm(form).onFormSubmit().create();
}
function wait(ms){
var start = new Date().getTime();
var end = start;
while(end < start + ms) {
end = new Date().getTime();
}
}
function onFormSubmit() {
wait(7000);
AddingUserAutomation.createUserFromSheets()
}
The trouble is I get the error
TypeError: Cannot read property 'getLastRow' of null
at createUserFromSheets(Code:43:19)
My createUserFromSheets function is taking the active sheet
function createUserFromSheets(){
let data = SpreadsheetApp.getActiveSheet();
let row = data.getLastRow();
let firstname = data.getRange(row,2).getValue();
let lastname = data.getRange(row,3).getValue();
... etc etc
}
I think it is unable to pull the getActiveSheet part that is why I had added the wait() function on formSubmit() but it still would not work.
Is there a way to solve this or a better way to do it?
function createWorkspaceUser(recentResponse) {
console.log("Creating account for:\n"+recentResponse[1]);
debugger;
var user = {"primaryEmail": recentResponse[0] + '.' + recentResponse[1] + '#' + recentResponse[3],
"name": {
"givenName": recentResponse[0],
"familyName": recentResponse[1]
},
"password": newPassword(),
};
try {
user = AdminDirectory.Users.insert(user);
console.log('User %s created with ID %s.', user.primaryEmail, user.id);
}catch(err) {
console.log('Failed with error %s', err.message);
}
}
I am doing it this way but it's running an error on primaryemail
Suggestion [NEW UPDATE]
As mentioned by RemcoE33
To have a more simplified setup, perhaps skip the library part and do all the scripting (bound script) in your Google Form itself.
Since we don't have the complete overview of your actual Google Form. See this sample below as a reference:
Google Form Script
function onFormSubmit() {
var form = FormApp.getActiveForm();
var count = 0;
var recentResponse = [];
var formResponses = form.getResponses();
for (var i in formResponses) {
count += 1;
var formResponse = formResponses[i];
var itemResponses = formResponse.getItemResponses();
for (var j = 0; j < itemResponses.length; j++) {
if(formResponses.length === count){ //Process only the recently submitted response
var itemResponse = itemResponses[j];
recentResponse.push(itemResponse.getResponse())
}
}
}
createWorkspaceUser(recentResponse);
}
function createWorkspaceUser(recentResponse){
var user = {"primaryEmail": recentResponse[0].replace(/\s/g, '') + '.' + recentResponse[1].replace(/\s/g, '') + '#' +recentResponse[3],
"name": {
"givenName": recentResponse[0],
"familyName": recentResponse[1]
},
"password":newPassword(),
};
try{
user = AdminDirectory.Users.insert(user);
Logger.log('User %s created with ID %s.', user.primaryEmail, user.id);
}catch (err) {
Logger.log('Failed with error %s', err.message);
}
console.log(user);
}
NOTE: You no longer need to build an on form submit trigger since the onFormSubmit() function will automatically run right after hitting the submit button.
Demonstration
1. Submit user data from sample form:
2. Test user account will be created on Workspace Admin Console Users:
Reference
https://developers.google.com/apps-script/reference/forms/form-response
https://developers.google.com/apps-script/guides/triggers

Permissions API Call Failing when used in onOpen simple trigger

I've created a Google Sheet with an Apps Script to do issue and task tracking. One of the features I'm trying to implement is permissions based assigning of tasks. As a precursor to that, I have a hidden sheet populated with a list of users and their file permissions, using code similar to that in this StackOverflow question
When I manually run the code, it works fine. However, I want it to load every time the sheet is opened in case of new people entering the group or people leaving the group. So I made a call to my function in my onOpen simple trigger. However, when it is called via onOpen, I get the following:
GoogleJsonResponseException: API call to drive.permissions.list failed with error: Login Required
at getPermissionsList(MetaProject:382:33)
at onOpen(MetaProject:44:3)
Here are my functions:
My onOpen Simple Trigger:
function onOpen() {
//Constant Definitions for this function
const name_Access = 'ACCESS';
const row_header = 1;
const col_user = 1;
const col_level = 2;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sht_Access = ss.getSheetByName(name_Access);
var ui = SpreadsheetApp.getUi();
ui.createMenu('MetaProject')
.addSubMenu(
ui.createMenu('View')
.addItem('Restore Default Sheet View', 'restoreDefaultView')
.addItem('Show All Sheets', 'showAllSheets')
)
.addSeparator()
.addToUi();
//Clear Contents, Leave Formatting
sht_Access.clearContents();
//Set Column Headers
var head_name = sht_Access.getRange(row_header,col_user);
var head_level = sht_Access.getRange(row_header,col_level);
head_name.setValue('User');
head_level.setValue('Level');
//Refresh User List for use in this instance
getPermissionsList();
}
Here is getPermissionsList:
function getPermissionsList() {
const fileId = "<REDACTED>"; // ID of your shared drive
const name_Sheet = 'ACCESS';
const row_start = 2;
const col_user = 1;
const col_access = 2;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var thissheet = ss.getSheetByName(name_Sheet);
// THIS IS IMPORTANT! The default value is false, so the call won't
// work with shared drives unless you change this via optional arguments
const args = {
supportsAllDrives: true
};
// Use advanced service to get the permissions list for the shared drive
let pList = Drive.Permissions.list(fileId, args);
//Put email and role in an array
let editors = pList.items;
for (var i = 0; i < editors.length; i++) {
let email = editors[i].emailAddress;
let role = editors[i].role;
//Populate columns with users and access levels / role
thissheet.getRange(row_start + i,col_user).setValue(email);
thissheet.getRange(row_start + i,col_access).setValue(role);
}
I searched before I posted and I found the answer via some related questions in the comments, but I didn't see one with an actual answer. The answer is, you cannot call for Permissions or anything requiring authorization from a Simple Trigger. You have to do an Installable trigger.
In order to do that do the following:
Create your function or rename it something other than "onOpen".
NOTE: If you name it onOpen (as I originally did and posted in this answer) it WILL work, but you will actually run TWO triggers - both the installable trigger AND the simple trigger. The simple trigger will generate an error log, so a different name is recommended.
Click the clock (Triggers) icon on the left hand side in Apps Script.
Click "+ Add Trigger" in the lower right
Select the name of your function for "which function to run"
Select "onOpen" for "select event type"
Click "Save".
The exact same code I have above now runs fine.

GAS onFormSubmit trigger returns after first console.log

Problem: for some users I can only see the first logToConsole(). Then it seems that the function returns without throwing an error or anything else.
What I have checked / tried so far:
Google Form saved in Shared Drive vs. My Drive
Creating new forms
function onSubmitInst(event) {
logToConsole('onSubmitInst start'); // Is logged
var form, docProps, valueStatus, formLimits, realFormValueCell, mailFormStatus, formTitle;
try {
docProps = PropertiesService.getDocumentProperties();
} catch (error) {
logToConsole('onSubmitInst property error'); // Is NOT logged
return;
}
form = event.source;
formTitle = form.getTitle();
mailFormStatus = docProps.getProperty('mailForm');
formLimits = docProps.getProperty('formLimits');
formLimits = formLimits ? JSON.parse(formLimits) : {};
logToConsole('onSubmitInst OK'); // Is NOT logged
// ... more code ...
}
From the documentation, PropertiesService.getDocumentProperties() returns a valid Properties object if the script is published as an add-on or it is bound to a document. Else it returns null.
Either way, it will not trigger your try -> catch, null returns are not exceptions and does not generate error messages.
You would have to check the returned object if null or valid and adjust your logic accordingly.
docProps = PropertiesService.getDocumentProperties();
if (docProps === null) {
logToConsole('onSubmitInst property returned null');
return;
}
References:
Class Properties Service

Disable multiple instances of onSubmit trigger during submissions

I have a form with onSubmit trigger, Which will be used as an domain wide add - on.The add - on can be used
for many forms.
I need to disable the multiple submit triggers, instead run only 1 instance currently.
Say the form submissions arrive at same time, I need to avoid the duplicate instances of the script running when the current script is running.
I tried to use lock service
function onFormSubmit(e) {
// lock the document response
var lock = LockService.getDocumentLock();
if (lock.hasLock()) return;
//do some tasks
lock.releaseLock();
}
How do I make a script (addon) to run as a single instance.
UPDATE: This code too, doesnt achive the intended functionality
function OnSubmits(e) {
var releaseLock;
releaseLock = function() {
lock.releaseLock();
}
var lock = LockService.getDocumentLock();
lock.tryLock(1);
if (!lock.hasLock()) {
return;
}
var value = cUseful.Utils.expBackoff(function() {
releaseLock();
return PropertiesService.getScriptProperties().getProperty('key');
});
if (value == null) value = 0;
else value++;
cUseful.Utils.expBackoff(function() {
var folder = DriveApp.getFolderById("1wFGMh38JGarJd8CaiaOynlB7iiL_Pw6D");
folder.addFile(DriveApp.getFileById(DocumentApp.create('Document Name ' + value).getId()));
PropertiesService.getScriptProperties().setProperty('key', value);
});
releaseLock();
}

Get the ID of a spreadsheet

So, I'm already trying this for a week, still errors. Can get the spreadsheet ID properly.
Currently I have this code:
function getSS(e,getSS) {
//If not yet authorized - get current spreadsheet
if (e && e.authMode != ScriptApp.AuthMode.FULL) {
var getSS = SpreadsheetApp.getActiveSpreadsheet();
}
//Else if authorized set/get property to open spreadsheet by ID for time-driven triggers
else {
if(!PropertiesService.getDocumentProperties().getProperty('SOURCE_DATA_ID')){
PropertiesService.getDocumentProperties().setProperty('SOURCE_DATA_ID', e.source.getId());
}
var getSSid = PropertiesService.getDocumentProperties().getProperty('SOURCE_DATA_ID');
var getSS = SpreadsheetApp.openById(getSSid);
}
return getSS;
}
var SS = getSS();
It's supposed to get active spreadsheet ID when the addon is not yet authorized, and get a spreadsheet ID from properties when it's authorized. However, when testing as installed, I always get an error that I don't have permission to use openById() or getDocumentProperties()
How do I keep SS as global variable without it being null in any authMode?
Note that global variables are constructed each and every time that Apps Script project is loaded / used. Also note that no parameters are passed to functions automatically - you have to designate a function as either a simple trigger (special function name) or an installed trigger before Google will send it an argument, and in all other cases you have to explicitly specify the argument.
The problem is then that you declare var SS = getSS(); in global scope, and do not pass it any parameters (there are no parameters you could pass it, either). Thus in the definition of getSS(), even if you have it as function getSS(e) {, there is no input argument to bind to the variable e.
Therefore this criteria if (e && ...) is always false, because e is undefined, which means your else branch is always executed. In your else branch, you assume that you have permissions, and your test never was able to even try to check that. Hence, your errors. You might have meant to write if (!e || e.authMode !== ScriptApp.AuthMode.FULL) which is true if either of the criteria is true. Consider reviewing JavaScript Logical Operators.
While you don't share how your code uses this spreadsheet, I'm quite certain it doesn't need to be available as an evaluated global. Any place you use your SS variable, you could have simply used SpreadsheetApp.getActiveSpreadsheet() instead.
Your getSS() function additionally force the use of a permissive scope by using openById - you cannot use the preferred ...spreadsheets.currentonly scope.
Example add-on code:
function onInstall(e) {
const wb = e.source;
const docProps = PropertiesService.getDocumentProperties();
docProps.setProperty('SOURCE_DATA_ID', wb.getId());
/**
* set other document properties, create triggers, etc.
*/
// Call the normal open event handler with elevated permissions.
onOpen(e);
}
function onOpen(e) {
if (!e || e.authMode === ScriptApp.AuthMode.NONE) {
// No event object, or we have no permissions.
} else {
// We have both an event object and either LIMITED or FULL AuthMode.
}
}
Consider reviewing the Apps Script guide to add-on authorization and setup: https://developers.google.com/apps-script/add-ons/lifecycle
So I made it this way:
//Because onInstall() only runs once and user might want to launch addon in different spreadsheets I moved getting ID to onOpen(),
function onInstall (e) {
getAuthUrl();
onOpen(e);
}
Cases for different AuthModes.
function onOpen(e) {
var menu = SpreadsheetApp.getUi().createAddonMenu();
if (e && e.authMode === ScriptApp.AuthMode.NONE) {
menu.addItem('Authorize this add-on', 'auth');
}
else {
//If addon is authorized add menu with functions that required it. Also get the id of the current spreadsheet and save it into properties, for use in other functions.
menu.addItem('Run', 'run');
var ssid = SpreadsheetApp.getActive().getId();
var docProps = PropertiesService.getDocumentProperties();
docProps.setProperty('SOURCE_DATA_ID', ssid);
}
menu.addToUi();
}
Function that pops authorization window:
function getAuthUrl() {
var authInfo,msg;
authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);
msg = 'This addon needs authorization to work properly on this spreadsheet. Click ' +
'this url to authorize: <br><br>' +
'<a href="' + authInfo.getAuthorizationUrl() +
'" style="cursor:pointer;padding:5px;background: #4285f4;border:1px #000;text-align: center;margin-top: 15px;width: calc(100% - 10px);font-weight: 600;color: #fff">AUTHORIZE</a>' +
'<br><br> This spreadsheet needs to either ' +
'be authorized or re-authorized.';
//return msg;//Use this for testing
//ScriptApp.AuthMode.FULL is the auth mode to check for since no other authorization mode requires
//that users grant authorization
if (authInfo.getAuthorizationStatus() === ScriptApp.AuthorizationStatus.REQUIRED) {
return msg;
} else {
return "No Authorization needed";
};
console.info('Authorization window called');
}