Apps Script OnChange not firing when spreadsheet is edited from elsewhere - javascript

So I have a very simple Apps script code that has OnChange trigger, and it fires whenever I edit the spreadsheet, then sends a post request to another server. It works perfectly when I add or remove a row from the spreadsheet, but if the spreadsheet is edited from an API or Appsheet for example, the trigger doesn't get fired, and so does the post request. Any suggestions?
function setUpTrigger() {
ScriptApp.newTrigger('logChanges').forSpreadsheet('ID HERE').onChange().create();
}
function logChanges(e) {
const sheet=SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sheetname')
if (e.changeType == "INSERT_ROW") {
var range = sheet.getActiveRange();
var row = range.getLastRow();
Utilities.sleep(3000)
let obj = sheet.getRange(row, 1, 1, 3).getValues().toString().split(",") // name, email, number
let data = {
'name': obj[0],
'email': obj[1],
'number': obj[2]
};
const params = {
'method': 'POST',
'contentType': 'applic`your text`ation/json',
'payload': JSON.stringify(data)
}
let res = UrlFetchApp.fetch('url', params)
}
}
I don't really know why it doesn't get fired when I'm not the one who directly changes it. I saw some questions related to Zapier and IFTTT, and the answers suggested changing the trigger from OnEdit to OnChange, but I already have it OnChange and it's not working.

Related

Zendesk Update Users API From Google Sheets

I'm going to start by saying it's immensely frustrating half knowing how to do something but never quite being able to finish; this is another one of those projects for me.
Scenario: Using a Google Sheet and Apps Script I am attempting to update several User records in Zendesk using their API.
I think i probably have most if it right (i stand to be corrected of course) with the following script however I just cannot get it to update any records. I suspect it might be to do with how the array is presented (an area I sadly don't know enough about).
function updateManyUsers(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var [headers, ...rows] = sheet.getDataRange().getValues();
var data = {}
var items = []
rows.forEach(function(r) {
var obj={}
r.forEach(function (c, j) {
obj[headers[j]] = c
})
var data = {}//moved
data['users'] = obj // moved this inside your loop
items.push(data) // pushed the object into the items array
})
Logger.log("Log JSON Stringify Items: " + JSON.stringify(items))
items.forEach(function(i) { // added this to loop over objects in items
var url = 'https://itsupportdesk1611575857.zendesk.com/api/v2/users/update_many.json'; //https://developer.zendesk.com/api-reference/ticketing/users/users/#update-user
var user = 'myemailaddresshere/token';
var pwd = 'mytoken';
var options = {
'method' : 'PUT',
'headers': {
'Authorization': "Basic " + Utilities.base64Encode(user + ':' + pwd)
},
'payload' : JSON.stringify(i),
'contentType': 'application/json',
'muteHttpExceptions': true
};
UrlFetchApp.fetch(url, options);
Logger.log(i)
var response = UrlFetchApp.fetch(url, options);
Logger.log(response);
})
}
I've gone through as much as I can following the documentation, I know i had the end points incorrect and the method(?) too (set to Post instead of Push). I have gone through varying error messages that I have tried to act upon and this is my current one:
This is an image of the data in my sheet
Suplimental: In order to get better at this i would like to put myself on a learning path but am unsure what the path is; most of my automation work and scripting is done using Google Apps script so would people recommend a JavaScript course? I alter between that and Python not knowing what would suit me best to get a better understanding of this kind of issue.
Many thanks in advance.
From your endpoint in your script, I thought that you might have wanted to use "Batch update". Ref If my understanding is correct, the following sample curl in the official document can be used. Ref
curl https://{subdomain}.zendesk.com/api/v2/users/update_many.json \
-d '{"users": [{"id": 10071, "name": "New Name", "organization_id": 1}, {"external_id": "123", "verified": true}]}' \
-H "Content-Type: application/json" -X PUT \
-v -u {email_address}:{password}
If this sample curl command is converted to Google Apps Script using your script, how about the following modification?
Modified script:
function updateManyUsers2() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var [headers, ...rows] = sheet.getDataRange().getDisplayValues();
var users = rows.map(r => {
var temp = {};
headers.forEach((h, j) => {
if (r[j] != "") temp[h] = r[j];
});
return temp;
});
var url = 'https://itsupportdesk1611575857.zendesk.com/api/v2/users/update_many.json';
var user = 'myemailaddresshere/token';
var pwd = 'mytoken';
var options = {
'method': 'PUT',
'headers': {
'Authorization': "Basic " + Utilities.base64Encode(user + ':' + pwd)
},
'payload': JSON.stringify({ users }),
'contentType': 'application/json',
'muteHttpExceptions': true
};
var response = UrlFetchApp.fetch(url, options);
Logger.log(response.getContentText());
}
Note:
From the official document, it says Bulk or batch updates up to 100 users.. So, when you want to use more data, please modify the above script. Please be careful about this.
If an error occurs, please check the values of users, user and pwd, again.
Reference:
fetch(url, params)

Sending data to Google Spreadsheet via Ajax

I'm having problems sending data to a Google Spreadsheet via Ajax.
I followed this guide to achieve this: https://medium.com/#dmccoy/how-to-submit-an-html-form-to-google-sheets-without-google-forms-b833952cc175
My own code looks as simple as this:
var data = {"user": "bar"};
var url = 'https://script.google.com/macros/s/AKfycby6BzLifopR36ZBJ2WqDMNsndCYUpHihSOfhomfSRZXcdetvOA/exec';
$('.my-button').on('click', function (e) {
$.ajax({
url: url,
method: "GET",
dataType: "json",
data: data
}).done(
function (__e) {
console.log('Remote sheet updated!', __e);
}
).fail(function (__e) {
console.log('Remote sheet update failed', __e);
});
});
This is what my spreadsheet looks like:
This is the code from the tutorial that goes into the Google scripteditor.
function doGet(e){
return handleResponse(e);
}
// Enter sheet name where data is to be written below
var SHEET_NAME = "DATA";
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
function handleResponse(e) {
// shortly after my original solution Google announced the LockService[1]
// this prevents concurrent access overwritting data
// [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
// we want a public lock, one that locks for all invocations
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("key", doc.getId());
}
In the code provided by the author of the tutorial all I did is change the sheet name in line 9 to "DATA"
// Enter sheet name where data is to be written below
var SHEET_NAME = "DATA";
...which is the name of the sheet.
The problem is now, when I click "my-button", it gets send successfully, but returns an error object. The error doesn't contain any additional information though. It seems to go into the "catch" section of the Google script, but doesn't inform me of what exactly went wrong. Now I don't even know how to debug this, let alone understand, what went wrong.
I already tried all kinds of weird things like using JSON.stringify, none of that helped. It should work like this anyway, so I have no idea what's the problem here.

Nodejs not applying script id value passed to it

I am writing a program in nodejs using the request-promise-native module to create a startup script,receive the script ID then create a server and apply the script ID, however even after gathering the script ID and passing it to the request to create the server it does not get applied. I have made sure that the value of the variable I am passing for the scriptID is in the correct form of data type but it still seems not to work for some reason
var scriptRequest = require('request-promise-native');
var serverRequest = require('request-promise-native');
var headers = {
'API-Key': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
};
var scriptOptions = {
url: "https://api.vultr.com/v1/startupscript/create",
method: "POST",
headers: headers,
form: {
name: "test script",
script: "#!/bin/bash \napt-get update (not the actual script, just shortened it since the script itself is long and as of now the vultr server doesn't even make it this far)"}
};
var serverOptions = {
url: 'https://api.vultr.com/v1/server/create',
method: 'POST',
headers: headers,
form: {
DCID: '6',
VPSPLANID: '202',
OSID: '215',
SCRIPTID: scriptResponse
}
};
var scriptResponse;
async function main(){
const response = await scriptRequest({...scriptOptions, json:true})
const json = response.SCRIPTID
scriptResponse = json
await serverRequest(serverOptions)
}
main()
as you can see I have had to make the scriptRequest variable global, if I try to set it in the main function it gives me an undefined error in the serverOptions block
EDIT: just to clarify, the script ID I want to apply to the scriptResponse variable is returned upon making the scriptRequest post request

How to get the file URL from file name in Google Sheets with correct Authorization via custom function/script

I would like to create a custom function that pulls a Drive URL from a file name in Google Sheets.
So, using the code below:
If I have a valid file name in cell A1
The function =getFile(A1) would return the URL
When I run my script from within the script editor, the return value works.
When I run the function getFile() from within my sheet, I get the error below.
My code:
function getFile(cell) {
var filename = encodeURI(cell);
var url = "https://www.googleapis.com/drive/v3/files?fields=files(id,name)&q=name+contains+'" + filename + "' and trashed=false";
var params = {
method: "GET",
headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
muteHttpExceptions: true
};
var res = UrlFetchApp.fetch(url, params).getContentText();
var json = JSON.parse(res);
return res; // outputs response below
if(json){
var objFiles = json.files[0];
var fileID = objFiles.id
var resURL = "https://docs.google.com/spreadsheets/d/" + fileID;
Logger.log(resURL);
//return resURL; // only works when run within script editor
}
}
Error:
"{
"error": {
"errors": [
{
"domain": "global",
"reason": "authError",
"message": "Invalid Credentials",
"locationType": "header",
"location": "Authorization"
}
],
"code": 401,
"message": "Invalid Credentials"
}
}
"
I'm guessing something's wrong with my Auth token. Can someone direct me to resolving this? Thanks in advance!
Custom functions runs as if run by a anonymous animal(user). ScriptApp.getOAuthToken will return a anonymous token without the required scopes. What you're attempting is not possible, unless the file in question is public.
References:
Custom functions permissions
Custom functions access services
This may be a solution for some needs.
My particular need was: loop through a column of file names and pull the Google Docs URL at a set interval. The code below just loops through filenames in "Column A" of "My Sheet" and returns the value into the adjacent cell of "Column B" (starting at row 2 because I had column headers). I'm not concerned about security because I'm only referencing internal organization files.
To get the code below to work you need to:
Google Sheet Doc Nav > Tools > Script Editor
Create a .gs file & input code below (Referencing The Respective Sheet
Within Script Editor > Edit > Current Project’s Triggers > Name Your Project
Within Script Editor > Edit > Current Project’s Triggers > Click on modal link “No triggers set up. Click here to add one now” > set your time-based trigger (reference replaceFileColumn in the select field within that modal)
My mistake was: thinking that I needed to use a custom function in each cell to do so. (I still don't fully understand the auth reasons why this wouldn't work, so if anyone could explain in lay-man's terms that would be fabulous; my solution is just a workaround for expediency's sake).
In my spreadsheet I have a time-driven trigger calling replaceFileColumn()
Hope this helps someone!
function getMyFile(cell) {
var filename = encodeURI(cell);
var files = DriveApp.getFilesByName(cell);
while (files.hasNext()) {
var file = files.next();
if(file){
var fileValue = file.getUrl();
return(fileValue);
};
};
}
function replaceFileColumn() {
var spreadsheet = SpreadsheetApp.getActive().getSheetByName('My Sheet');
var range = spreadsheet.getRange("A2:A");
var range_update = spreadsheet.getRange("B2:B");
var values = range.getValues();
for (var i = 0; i < values.length; i++) {
var fileName = values[i];
var getFileUrl = getMyFile(fileName);
values[i][0] = getFileUrl;
}
range_update.setValues(values);
}
#I'-'I's answer is correct. Although I'm not sure whether this is what you want, how about this workaround? I have also experienced the same issue. At that time, I had used the following workaround.
Set and get access token using PropertiesService.
The flow is as follows.
Flow:
Set access token every 1 hour by the time-driven trigger.
By this, the access token is updated every 1 hour. Because the expiration time of access token is 1 hour.
When the custom function is run, it gets the access token using PropertiesService.
By this, the access token can be used.
Modified script:
Please install this function as the time-driven trigger. Of course, you can run manually this function.
function setAccessToken() {
PropertiesService.getScriptProperties().setProperty("accessToken", ScriptApp.getOAuthToken());
}
In your script, please modify as follows.
From:
var params = {
method: "GET",
headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
muteHttpExceptions: true
};
To:
var params = {
method: "GET",
headers: {"Authorization": "Bearer " + PropertiesService.getScriptProperties().getProperty("accessToken")},
muteHttpExceptions: true
};
Note:
In this case, the owner of access token is the owner of project.
I think that this can be also used by CacheService.
Reference:
PropertiesService

Debugging multiple Google form submissions on single click?

I'm trying to do a kind of purchase request app built off of a Google Spreadsheet. For awhile, (like the whole time I've been working on this), my code was working. For each line in the order sheet, it would loop through the values, fill in the Google form inputs, submit the form, then start the process again.
Yesterday I noticed it was submitting each line twice submitting the first line once, second line twice, third line three times, and so on. Then it stopped submitting at all. Then it started again submitting multiple times, then stopped. Could you guys please take a look at my code and tell me what I'm doing wrong?
function formSubmit() {
//Create unique ID (number of milliseconds since 1/1/70)
var d = new Date();
var n = d.getTime();
var uniqueID = n.toString();
//Loop through the lines of the order, fill in the values, submit
$('.orderline').each(function(i, obj) {
//Stop the default redirect so we can submit multiple times
$('#ss-form').submit(function(e) {
e.preventDefault();
$.ajax({
url: "https://docs.google.com/a/vt.edu/forms/d/e/1FAIpQLSf77MuDLeqyPbuDCBcpVagi6-hdiUpgZtr0CbuJ3kO-vXPswg/formResponse",
data: $(this).serialize(),
type: "POST",
dataType: "jsonp",
success: function(data) {
console.log("Submission successful");
},
error: function(xhr, status, error) {
console.log("Submission failed: " + error);
},
});
});
$("#entry_1725077600").val(uniqueID);
var name = $("#personname").val();
var email = $("#personemail").val();
$("#entry_1352722479").val(name);
$("#entry_1024015951").val(email);
//etc.
$("#ss-form").submit();
});
The form is public if you guys want to take a look. Note I have two forms submitting at once on the same click; the one above is for the items in the order, the second one is for metadata about the order.
EDIT: formSubmit() is being called from a second function that uploads files to Google Drive (if there's a better way to do this please do let me know):
if(document.getElementById('fUpload').value!='') {
var user = gapi.auth2.getAuthInstance().currentUser.get();
var oauthToken = user.getAuthResponse().access_token;
var uploadObj = $("[id$=fUpload]");
var file = uploadObj.prop("files")[0];
var metadata = {
'title': file.name,
'description': " ",
'mimeType': file.type || 'application/octet-stream',
"parents": [{
"kind": "drive#file",
"id": "0B5zM5ktmwJ2fN0c3RWYxWC1rUzQ"
}]
};
var arrayBufferView = new Uint8Array(file);
var uploadData = new Blob(arrayBufferView, {type: file.type || 'application/octet-stream'});
try{
var uploader =new MediaUploader({
file: file,
token: oauthToken,
metadata: metadata,
params: {
convert:false,
ocr: false
}
});
uploader.upload();
} catch(exc){
showErrorMessage("Error: " + exc);
$("#fUpload").val(" ");
}
} else {
formSubmit();
}
});
And then for successful responses:
MediaUploader.prototype.onContentUploadSuccess_ = function (e) {
if (e.target.status == 200 || e.target.status == 201) {
var response = e.target.response; //Get the response body
var parsed = JSON.parse(response); //Parse the response body to JS object
var fileID = parsed.id; //Get the file ID from the response
var linkToFile = "https://drive.google.com/open?id=" + fileID;
$("#entry_1703377267").val(linkToFile); //Add the file ID as the value of the file ID input field
formSubmit(); //Run the rest of the form submit functions
this.onComplete(e.target.response);
} else if (e.target.status == 308) {
this.extractRange_(e.target);
this.retryHandler.reset();
this.sendFile_();
}
};
EDIT 2: I never see success or error messages in the console. Also, it looks like things aren't being submitted twice, they're being submitted in a pattern: first item once, second item twice, third item three times, etc.

Categories