Monitor google sheet change event externally [duplicate] - javascript

What I wish to achieve:
Whenever a cell is changed in any google sheet on my shared drive (by
any user on the domain) I want to call an API endpoint and include
information about which cell was edited.
My approach:
I believe Google App Scripts Add-on is what I need. Installed for all users on the domain.
I see there are "bound" scripts and standalone scripts. For standalone scripts I am not able to create any other triggers than timer and calender based triggers. Bound scripts seem to be permanently bound to a single sheet and won't impact other sheets in any way.
What am I missing?
I find a few end-to-end tutorials on blogs for making bound scripts, but nothing for generic cross-domain stuff.

You can achieve all this through a standalone script. Create a standalone script and follow these steps:
Step 1: Get spreadsheet ids
First you would have to get the id of the different Spreadsheets in your shared drive. You can do it in Google Apps Script itself if you use the Advanced Drive Service (see Reference below). To activate this service, go to Resources > Advanced Google services... in your script editor and enable Drive API.
Then, write a function that will return an array of the spreadsheet ids in the shared drive. You will have to call Drive.Files.list for that. It could be something along the following lines (please write your shared driveId in the corresponding line):
function getFileIds() {
var params = {
corpora: "drive",
driveId: "your-shared-drive-id", // Please change this accordingly
includeItemsFromAllDrives: true,
q: "mimeType = 'application/vnd.google-apps.spreadsheet'",
supportsAllDrives: true
}
var files = Drive.Files.list(params)["items"];
var ids = files.map(function(file) {
return file["id"];
})
return ids;
}
Step 2: Create triggers for each spreadsheet
Install an onEdit trigger programmatically for each of the spreadsheets (an edit trigger fires a function every time the corresponding spreadsheet is edited, so I assume this is the trigger you want). For this, the ids retrieved in step 1 will be used. It could be something similar to this:
function createTriggers(ids) {
ids.forEach(function(id) {
var ss = SpreadsheetApp.openById(id);
createTrigger(ss);
})
}
function createTrigger(ss) {
ScriptApp.newTrigger('sendDataOnEdit')
.forSpreadsheet(ss)
.onEdit()
.create();
}
The function createTriggers gets an array of ids as a parameter and, for each id, creates an onEdit trigger: everytime any of these spreadsheets is edited, the function sendDataOnEdit will run, and that's where you will want to call your API endpoint with information about the edited cell.
Step 3: Call API endpoint
The function sendDataOnEdit has to get data from the edited cell and send it somewhere.
function sendDataOnEdit(e) {
// Please fill this up accordingly
var range = e.range;
var value = range.getValue();
UrlFetchApp.fetch(url, params) // Please fill this up accordingly
}
First, it can get information about the cell that was edited via the event object, passed to the function as the parameter e (you can get its column, its row, its value, the sheet and the spreadsheet where it is located, etc.). For example, to retrieve the value of the cell you can do e.range.getValue(). Check the link I provide in reference to get more details on this.
Second, when you have correctly retrieved the data you want to send, you can use UrlFetchApp.fetch(url, params) to make a request to your URL. In the link I provide below, you can see the parameters you can specify here (e.g., HTTP method, payload, etc.).
Please bear in mind that you might need to grant some authorization to access the API endpoint, if this is not public. Check the OAuth reference I attach below.
(You have to edit this function accordingly to retrieve and send exactly what you want. What I wrote is an example).
Summing this up:
In order to create the triggers you should run createTriggers once (if you run it more times, it will start creating duplicates). Run for example, this function, that first gets the file ids via Drive API and then creates the corresponding triggers:
function main() {
var ids = getFileIds();
createTriggers(ids);
}
Also, it would be useful to have a function that will delete all the triggers. Run this in case you want to start from fresh and make sure you don't have duplicates:
function deleteTriggers() {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(trigger) {
ScriptApp.deleteTrigger(trigger);
})
}
Reference:
Advanced Drive Service
Drive.Files.list
onEdit trigger
Install trigger programmatically
onEdit event object
UrlFetchApp.fetch(url, params)
Connecting to external APIs
OAuth2 for Apps Script
ScriptApp.deleteTrigger(trigger)
I hope this is of any help.

Related

Google App script Remove Editors From Protection

In Google Sheets I am trying to remove editors using script from all the protection except the sheet owner and for this I am using below code, but after running the code the entire protection is being removed, however in place of removing the protection I want to remove all the user from the protection except the sheet owner. Further, when the code is run only one protection is removed at a time I want to apply it for all the protection.
function remove(e) {
var sheet = SpreadsheetApp.getActiveSheet();
var protection = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE)[0];
if (protection && protection.canEdit()) {
protection.remove();
}}
Any help on above will be appreciated.
The changes that you should do to make the code only allow the spreadsheet owner be able to edit a protected range were already included on the Tainake's answer to your previous question Google Sheet Remove Editors After First Edit
From the question:
Further, when the code is run only one protection is removed at a time I want to apply it for all the protection.
sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE) returns an array of objects. To remove all the the protections your code should have to iterate over all the elements of this array. One of many ways to do this is by using Array.prototype.forEach, i.e.
function remove() {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE)
.forEach(protection => {
if (protection && protection.canEdit()) {
protection.remove();
}
});
}
Resources
https://developers.google.com/apps-script/guides/sheets
https://developers.google.com/apps-script/reference/spreadsheet/protection

Concurrent uses of Google Scripts

I've created a simple signup form that takes in the persons name & email, saves it to a database and gives him a seven digit code in return from a database. Each code can only belong to one person. The code is run under my user from Google App Scripts.
I'm now wondering, if I need to use Lockservice or anything else to allow for concurrent use of the program? If the 2 people use this program at the same time for example, would this likely cause any problems - in example that the input.name would be from the answers of one user accessing the script and input.email would be originated from another? A simple illustration of my code
document.getElementById("registerBtn").addEventListener("click",register);
function register (){
//take values from input
var input = {};
input.name = document.getElementById("namefield").value;
input.email = document.getElementById("emailfield").value;
if (input.email && input.name) {
google.script.run.withSuccessHandler(successfun).withFailureHandler(failurefun).checkInfo(input)
}else{
M.toast({html: 'Please insert your data'});
}}
function successfun (output){
M.toast({html: 'Your code is:' + output});
}
function failurefun (output){
M.toast({html: 'This name or email have already been registered'});
}
/// ... Google Scripts:
function checkInfo(input) {
// open google spreadsheet
//check with indexOf if the email exists
//if the email does not exist, check if name exists
//if the name does not exist - append new row.
//if this is successful, open up another tab and take the first value from there, that does not have 2 //next to it & change the value next to it to 2.
//return confirmation code;
//If name or email exist: return error;
}
// Editing the question taking the guidance from the comment into account: If I'd have the script run not from my account but from each individual user, I would not have this issue?
According to the Apps Script Lock Service documentation:
Lock Service allows scripts to prevent concurrent access to sections of code. This can be useful when you have multiple users or processes modifying a shared resource and want to prevent collisions.
Thus, since this is an Apps Script service, it can be used only in your Apps Script code and later called from the HTML code.
You might also want to take a look at the Apps Script's quotas, which states that you can get at most 30 simultaneously executions.
An alternative to your approach is the solution proposed in this answer here which suggests to make use of the Utilities.getUuid().
Reference
Apps Script Lock Service;
Apps Script Quotas;
How can I facilitate concurrent users on a Google Apps Script Webapp?;
Apps Script Utilites Class - getUuid().

Exception: You do not have permission to call DriveApp.createFile

I created a custom script to be run from google sheets which I need to create a file. The script uses the following code excerpt:
/*
Custom function to call IEXAPI IEXkeystatearningsdate
#customfunction
*/
function IEXkeystatearningsdate(inputsymbol, stat, version) {
if (version == "Sandbox" || version == "sandbox")
var url="https://sandbox.iexapis.com/stable/stock/"+inputsymbol+"/stats/"+stat+"?token=xyz";
else
var url="https://cloud.iexapis.com/stable/stock/"+inputsymbol+"/stats/"+stat+"?token=xyz";
var response=UrlFetchApp.fetch(url); //Call REST API
var json=response.getContentText();
var data = JSON.parse(json); //Parse into JSON object
var d = new Date();
var n = d.toLocaleString();
var fn = "IEXkeystatearningsdate_" + n;
DriveApp.createFile(fn,inputsymbol, MimeType.CSV);
return (data);
}
However, I receive this message:
"Exception: You do not have permission to call DriveApp.createFile. Required permissions: https://www.googleapis.com/auth/drive (line 20)"
When I run this script directly from the script editor, I don't receive this message.
This is my manifest file:
{
"oauthScopes": [
"https://www.googleapis.com/auth/drive"
],
"timeZone": "America/Los_Angeles",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
I do not use G-Suite. I only use google/sheets for my personal use. The OAuth FAQ says this call should be allowed for personal use. Can someone help me with what I need to do to get this to work?
From your error description I can assume that you are running your script on a simple onEdit trigger.
Simple onEdit trigger cannot access services that require authorization.
See restrictions.
For you it means that you can perform to DriveApp on simple trigger.
Solution:
Use an Installable trigger instead.
To do so:
Rename the function onEdit() to something else that is not a key word
Go on Edit -> Current project's triggers
Click on + to create a new trigger
Specify the funciton to run
Set Select event type to On edit
Custom functions cannot be used to call services that access personal data:
Unlike most other types of Apps Scripts, custom functions never ask users to authorize access to personal data. Consequently, they can only call services that do not have access to personal data
That, of course, includes DriveApp. And it also includes things like getOAuthToken() (it returns null when called via custom functions), so calling Drive API directly through UrlFetch cannot work either.
You should either install an onEdit trigger, as ziganotschka suggested, or call this function through a custom menu instead of a custom function, so that you're asked for authorization when trying to run it.
Reference:
Using Apps Script services

spreadsheets.values.get but with other sheets

So I was trying to read values from a spreadsheet in JS (not Node), and I came across https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get. This looks like the right thing for me, but what if I'm trying to read values from different sheets?
For example, how would I use spreadsheets.values.get to read A1:D1 in Sheet 2 in https://docs.google.com/spreadsheets/d/e/2PACX-1vQLrcAW_bL5HSVHorBJisdwv8S5_6Th9EP2wiYLmJKktj41uXVepUNOx4USNGdsVAuDOH_qknWs3pGa/pubhtml
Or is there another method I'm missing out on?
I believe your goal as follows.
You want to retrieve the values from the cells A1:D1 in the sheet of Sheet2.
You want to achieve this using the method of spreadsheets.values.get in Sheets API with Javascript which is not Node.js.
You have already been able to get values from Google Spreadsheet using Sheets API.
For this, how about this answer?
Pattern 1:
In this pattern, the function of makeApiCall() of the sample script (Browser) of this official document is used.
Sample script:
var params = {
spreadsheetId: '###', // Please set the Spreadsheet ID.
range: 'Sheet2!A1:D1', // Please set the range as a1Notation.
};
var request = gapi.client.sheets.spreadsheets.values.get(params);
request.then(function(response) {
console.log(response.result.values);
}, function(reason) {
console.error('error: ' + reason.result.error.message);
});
The spreadsheet ID is different from 2PACX-.... So please be careful this. Please check this official document.
In this case, it supposes that you have already done the authorization process for using Sheets API.
Pattern 2:
In this pattern, your URL like https://docs.google.com/spreadsheets/d/e/2PACX-... is used. From your URL in your question, it is found that the Spreadsheet is published to Web. In this case, only when the values are retrieved from the web published Spreadsheet, you can also use the following script.
Sample script:
const url = "https://docs.google.com/spreadsheets/d/e/2PACX-1vQLrcAW_bL5HSVHorBJisdwv8S5_6Th9EP2wiYLmJKktj41uXVepUNOx4USNGdsVAuDOH_qknWs3pGa/pub?gid=###&range=A1%3AD1&output=csv";
fetch(url).then(res => res.text()).then(txt => console.log(txt));
In this case, please set the sheet ID to ### of gid=###.
References:
Spreadsheet ID
Sheet ID

How to create a new row at the end of an existing sheet in Google Spreadsheets after completing a Google Form?

I'm trying to save responses from a linked Google Form into an existing Google sheet. More precisely, the spreadsheet shows employee's time off requests with the employee's Reason for leave, Number of days requested, Date Requested, Name of Manager, etc. Basically, the form asks the employee for these information.
Thanks!
What I'm trying to achieve here is saving the response information into the already created spreadsheet. According to Google's documentation, the only two options are: 1) Create a new spreadsheet: Creates a spreadsheet for responses in Google Sheets
2) Select existing spreadsheet: Choose from your existing spreadsheets in Google Sheets to store responses
In case of option #2, I can save responses in the same spreadsheet BUT it saves them in a new sheet; I'm trying to save the responses in the same
sheet.
I know that Google Form has a Script Editor. Would it be possible to run a function that sends the responses to the Spreadsheet? Maybe something like this?
function sendToSpreadSheet(e)
{
var ss = SpreadsheetApp.openById("abc1234567");
// Send response to spreadsheet
// Populate cells accordingly
}
I don't have much experience with Google Forms, so I'm not sure how to approach this issue. Any suggestion(s) is greatly appreciated.
Try this:
You have to supply the Spreadsheet Id and SheetName. And also create a onFormSubmit trigger for the spreadsheet.
function saveResponse(e) {
SpreadsheetApp.getActive().getSheetByName('SheetName').append(e.values);
}
or
function saveResponse(e) {
SpreadsheetApp.openById('id').getSheetByName('SheetName').append(e.values);
}
onFormSubmit Event Object for Spreadsheet
You can create trigger with something like this:
function createSetResponseTrigger(){
createTrigger('setResponse');
}
function createTrigger(funcname) {
if(!isTrigger(funcname)) {
ScriptApp.newTrigger(funcname).forSpreadsheet(SpreadsheetApp.getActive()).onFormSubmit().create();
}
}
and this
function isTrigger(funcName){
var r=false;
if(funcName){
var allTriggers=ScriptApp.getProjectTriggers();
for(var i=0;i<allTriggers.length;i++){
if(funcName==allTriggers[i].getHandlerFunction()){
r=true;
break;
}
}
}
return r;
}
or you can just create it manually with Edit Menu/Current Project Triggers.

Categories