I have an Google sheet add-on that uses a clock based trigger to make an api call to Google My Business API with a function called uploadPosts().
The User can set the trigger via the addon's menu like this:
function createTrigger() {
try{
deleteTriggers();
let ss = SpreadsheetApp.getActiveSpreadsheet();
let configTab = ss.getSheetByName('CONFIG');
let hour = configTab.getRange('A3').getValue();
let hourVal = hour.toString().split(" - ")[0].trim();
ScriptApp.newTrigger('uploadPosts')
.timeBased()
.atHour(hourVal)
.nearMinute(0)
.everyDays(1)
.create();
}
catch(err) {
console.log(err);
if (err.message === "Cannot read property 'getRange' of null"){
Browser.msgBox("😩 Woa there! You must run Initial Setup, before turning on Auto-Posting! 😩 ");
}
}
}
function deleteTriggers () {
try{
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
catch(err) {
if (err.message === "Cannot read property 'getRange' of null"){
Browser.msgBox("😩 Woa there! You must run Initial Setup, before turning off Auto-Posting! 😩 ");
}
}
}
The triggers work fine for anyone who is an editor of the Add-on, but does not set a functioning trigger for add-on users. When they go to https://script.google.com/home/triggers they see a row with a "-" set for both the project name and function name.
What do I need to change in order for a clock based trigger to work for add-on users?
The solution to this issue was twofold.
The project used the newer V8 runtime. There were tons of issues in the apps script google groups re: the new runtime and time based triggers for add-ons.
I Modified all functions to revert to es5 syntax and then reverted to the old runtime.
I needed to add a sensitive scope: https://www.googleapis.com/auth/script.scriptapp
After I got approved by Google and reverting runtimes, my triggers worked as anticipated.
Related
I have a general Google Sheets document title "Roadmap".
I want to use a copy of it to share will all my students.
Eg. "John Doe Roadmap", "Jame Smith Roadmap"...
And I want datas sent to a webhook (for Zapier) when someone work on one of the sheets, to update automatically a sheet with "Students Progress".
For that, I need the code to be triggered each time the sheet is modified.
❌ I tried with triggers menu : but the trigger is not copied with the Spreadsheet when I create a copy.
❌ I tried with a simple trigger (onEdit()) : it's not authorised and I need to go in the code and execute it a First time manually to add authorisation. It's not good as I need to automate the process.
It seems the solution is to use an installed trigger. I added it in the code below... But... How to have the installation of the trigger... automatically triggered ?
The code is well copied with the copies of the main document, but if I don't go to the code to execute manually the function createEditTrigger(), the trigger is not installed and the code isn't triggered when someone modify the copied document.
I don't know how to do.
Here's my code:
function createEditTrigger() {
ScriptApp.newTrigger("sendZap")
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
function sendZap() {
let ss = SpreadsheetApp.getActive();
let activeSheet = SpreadsheetApp.getActiveSheet();
var month = activeSheet.getName();
var sh1=ss.getSheetByName('1er mois');
var emailMember = sh1.getRange(5, 4).getValue();
let data = {
'currentMonth': month,
'email': emailMember,
};
const params = {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(data)
}
let res = UrlFetchApp.fetch('https://hooks.zapier.com/hooks/catch/XXXXXXXX/', params)
SpreadsheetApp.getUi().alert("month: " + month + " email: " + emailMember);
}
Thank you.
Update
Perhaps it doesn't work because, with programatically added trigger too it asks for permission when I run the function (in the code window). How to avoid this authorisation as it's only used with my own account for all ss?
Said differently: when I save a copy of the ss, it saves the code attached too. But how can I copy the triggers too?
Maybe you can try this way. The goal is to make a menu and in this way, ask to activate the trigger if it is not already active.
function onOpen() {
SpreadsheetApp.getUi().createMenu('🌟 Menu 🌟')
.addItem('👉 Activate', 'activate')
.addToUi();
}
function activate(){
myTriggerSetup()
SpreadsheetApp.getActive().toast('your script is now active !')
}
function myTriggerSetup() {
if(!isTrigger('sendZap')) {
ScriptApp.newTrigger('sendZap')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
}
function isTrigger(funcName) {
var r=false;
if(funcName) {
var allTriggers=ScriptApp.getProjectTriggers();
var allHandlers=[];
for(var i=0;i<allTriggers.length;i++) {
allHandlers.push(allTriggers[i].getHandlerFunction());
}
if(allHandlers.indexOf(funcName)>-1) {
r=true;
}
}
return r;
}
I’m a newbie with everything related to Google Apps Scripts and I’m trying to implement a password with a Prompt that shows every time the document is opened, so every person that tries to access the Google Sheets has to type a password, otherwise they won’t be able to edit it. It kind of works, the thing is that if I (the owner of the document) haven’t unlocked it myself, the prompt won’t show to the other editors. Is this the correct way to do so?
function onOpen() {
protectSheet();
password();
}
function protectSheet() {
var sheet = SpreadsheetApp.getActiveSheet();
//Protect the sheet
var protection = sheet.protect().setDescription("Only owner can edit");
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
Logger.log(protection.getEditors());
Logger.log(Session.getEffectiveUser());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
}
function unprotectSheet() {
var sheet = SpreadsheetApp.getActiveSheet();
//Unprotect sheet
var unprotection = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
if (unprotection && unprotection.canEdit()) {
unprotection.remove();
Logger.log("Entró: "+unprotection.getEditors());
}
}
function password() {
var password = "123";
var textoIngresado;
do {
var passwordPrompt = SpreadsheetApp.getUi().prompt("Protected document", "Type password", SpreadsheetApp.getUi().ButtonSet.OK_CANCEL);
if (passwordPrompt.getSelectedButton() == SpreadsheetApp.getUi().Button.CANCEL) {
protectSheet();
console.log("CANCELLED, PROTECTED");
break;
} else if (passwordPrompt.getSelectedButton == SpreadsheetApp.getUi().Button.CLOSE) {
protectSheet();
console.log("CLOSED, PROTECTED");
break;
}
textoIngresado = passwordPrompt.getResponseText();
}
while (textoIngresado != password);
if (textoIngresado == password) {
SpreadsheetApp.getActiveSheet().getRange("A7").setValue("Unlocked");
unprotectSheet();
console.log("UNLOCKED");
}
}
The code is using a onOpen simple trigger. It looks that you are missing that simple triggers have limitations (for details see https://developers.google.com/apps-script/guides/triggers)
Instead of a simple trigger, use an installable trigger (for details see https://developers.google.com/apps-script/guides/triggers/installable)
Regarding using a custom password it looks that you are trying "to discover the hot water". Instead of creating a custom access control system the best is to use the Google Drive sharing system:
change the sharing settings i.e. change from editor to viewer.
instead of changing the settings by using individual emails you might use a Google Group
Related
Google Sheets Appscript pop up prompt response and hide sheets until response is correct
Fairly new to app script so bare with me.
Wrote this massive script, then went to set it up on a times trigger and it just refuses to run. I've gone ahead an back tracked as much as I could, to get at least something to work, yet I can't even get a basic toast to appear on a minute interval.
This is the script I've built, which I'm running directly to enable the trigger:
function createTriggers() {
ScriptApp.newTrigger('testTime')
.timeBased()
.everyMinutes(1)
.create();
};
The function it's calling is super simple, I've used it a lot and change it a lot too:
var gSS = SpreadsheetApp.openById("this_1s/the.1d")
function testTime() {
var d = new Date()
var Start = d.getTime();
gSS.toast(Start, "Testing", 30)
};
So how it should work, and it does if I just call the 'testTime' function directly, is a little toast pop-up appears on the spreadsheet in question, and stays visible for 30s.
When I run the trigger function 'createTriggers', nothing happens..
Please help! All the code I wrote is for nothing if I can't get it to run on its own.. :(
***** EDIT - 08/04/20 - based on comments *****
It's possible this was an XY example, I tried to run a small segment of the original code which works when I run it directly, and its not working here either.. this snippit does not have any UI facing functions in it, so it shouldn't be the issue..
All i did was take the above trigger function and change the name to 'testClear', which calls to the following functions:
function testClear(){
sheetVars(1)
clearSheetData(sheetSPChange)
};
function sheetVars(numSprints) {
// returns the global vars for this script
try {
sheetNameSprints = "Name of Sprint Sheet"
sheetNameSPChange = "Name of Story Point Change Sheet"
sheetSprints = gSS.getSheetByName(sheetNameSprints)
sheetSPChange = gSS.getSheetByName(sheetNameSPChange)
arraySprints = iterateColumn(sheetSprints,"sprintIDSorted", 1, numSprints)
}
catch(err) {
Logger.log(err)
};
};
function iterateColumn(sheet, header, columnNum, numRows) {
// Create an array of first column values to iterate through
// numRows is an int, except for the string "all"
var gData = sheet.getDataRange();
var gVals = gData.getValues();
var gLastR = ""
var gArray = []
// check how many rows to iterate
if (numRows == "all") {
gLastR = gData.getLastRow();
}
else {
gLastR = numRows
};
// Iterate through each row of columnNum column
for (i = 1; i < gLastR; i++){
// iterate through
if(gVals[i][columnNum] !== "" && gVals[i][columnNum] !== header){
// push to array
gArray.push(gVals[i][columnNum]);
}
else if (gVals[i][columnNum] == "") {
break
};
};
return gArray
};
function clearSheetData(sheet) {
// Delete all rows with data in them in a sheet
try {
if (!sheet.getRange(sheet.getLastRow(),1).isBlank()){
sheet.getRange(2, 1, sheet.getLastRow()-1, sheet.getLastColumn()-1).clearContent()
Logger.log("Sheet cleared from old data.")
}
else {
sheet.deleteRows(2, sheet.getLastRow()-1)
Logger.log("Sheet rows deleted from old data.")
};
}
catch(err){
Logger.log(err)
emailLogs()
};
};
The 'emailLogs' function is a basic MailApp so i get notified of an issue with the script:
function emailLogs() {
// Email Nikita the loggs of the script on error
var email = "my work email#jobbie"
var subject = "Error in Sheet: " + gSS.getName()
var message = Logger.getLog()
MailApp.sendEmail(email, subject, message)
};
Thanks to a comment I've now discovered the executions page!! :D This was the error for the edited script.
Aug 4, 2020, 10:48:18 AM Error Exception: Cannot call
SpreadsheetApp.getUi() from this context.
at unknown function
To show a toast every certain "time" (every n iterations) add this to the for loop
if (!((i+1) % n)) spreadsheet.toast("Working...")
From the question
Aug 4, 2020, 10:48:18 AM Error Exception: Cannot call SpreadsheetApp.getUi() from this context. at unknown function
The above error means that your time-drive script is calling a method that can only be executed when a user has opened the spreadsheet in the web browser.
In other words, toast can't be used in a time-driven trigger. The solution is to use client-side code to show that message every minute. To do this you could use a sidebar and a recursive function that executes a setTimeout
References
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals
Based on all the comments, and new things I'd learned from that..:
I'd been calling to a global variable for my onOpen function:
var gUI = SpreadsheetApp.getUi();
even though I wasn't using it for the trigger, since it was global it tried to run and then failed.
I moved the actual definition of gUI into the onOpen function, and tried again and it worked.
Thank you all for the support!
I'm newbie here...
I've created a function to copy sprint data from my breakdown sheet. Everytime I call function on google spreadsheet, an error show up stating I need permission although I have given that function required permission on setting up trigger (the trigger I used is 'on form submit'). I'm still confused with triggers and functions like 'onEdit'&'onOpen' . Can anybody explain it to me, here's my function
function Copy() {
var copy=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Breakdown");
var paste=SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var row=copy.getLastRow();
var d=7;
for (var i=1;i<=row;i++)
{ if (copy.getRange(i,8).getValue() == "Sprint")
{var c=copy.getRange(i,1).getValue();
paste.getRange(d,1).setValue(c);
c=copy.getRange(i,2).getValue();
paste.getRange(d,2).setValue(c);
c=copy.getRange(i,4).getValue();
paste.getRange(d,3).setValue(c);
c=copy.getRange(i,5).getValue();
paste.getRange(d,4).setValue(c);
c=copy.getRange(i,6).getValue();
paste.getRange(d,5).setValue(c);
d++;
}
}
}
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');
}