Get the ID of a spreadsheet - google-apps-script

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');
}

Related

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.

Issue running an Installed Trigger in Google App Script

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!

"Cannot call SpreadsheetApp.getUi() from this context" error while not using getUi() on time-based trigger

I am trying to run a function every night that checks a list of dates and does work if it finds that a date has passed and all the checkboxes on that row are checked. Every day, though, I get an email saying
"Cannot call SpreadsheetApp.getUi() from this context. (line 172, file "Code")".
The weird thing is that I don't use getUi() anywhere in my CheckHireDates function and the line that it specifies is not even in the function that is supposed to run. Line 172 is in my onEdit function which also doesn't use getUi(). Can anybody help me understand why I'm getting this error?
I did use getUi in my onOpen function, so I commented it out when I started having this problem but it still didn't fix anything.
function CheckHireDates() {
var spreadsheet = SpreadsheetApp.getActive();
var PaycomSheet = spreadsheet.getSheetByName('Paycom');
var TechSheet = spreadsheet.getSheetByName("Tech Key");
var SoftwareTracker = spreadsheet.getSheetByName('Software Tracking');
var range = "R2:R";
var Hvals = PaycomSheet.getRange("H2:H").getValues();
var Hlast = Hvals.filter(String).length;
var data = PaycomSheet.getRange(range).getValues();
var today = new Date().toLocaleDateString();
for(var i = 0; i < Hlast;i++)
{
Hvals[i].toLocaleString();
if(Hvals[i] <= today && (PaycomSheet.getRange('R' + (i+2)).getValue() == true))
{
var fullName = PaycomSheet.getRange('D' + (i+2)).getValue();
var techRow = findMatchingRow(fullName, "Tech Key");
var softwareRow = findMatchingRow(fullName, "Software Tracking");
var position = PaycomSheet.getRange('G' + (i+2)).getValue();
TechSheet.getRange('E' + techRow).setValue(1);
SoftwareTracker.getRange('G' + softwareRow).setValue(1);
if (position == "Pre-Employment, Initial")
{
position = "Initial";
}
else if (position == "Pre-Employment, Ace")
{
position = "Route";
}
else if (position == "Pre-Employment, Expert")
{
position = "Route";
}
else
{
Logger.log("Position not known");
}
TechSheet.getRange('G' + techRow).setValue(position);
SoftwareTracker.getRange('D' + softwareRow).setValue(position);
SoftwareTracker.getRange('H' + softwareRow + ':M' + softwareRow).setValue(2);
if (position !== "Initial")
{
SoftwareTracker.getRange('N' + softwareRow).setValue(2);
}
}
}
}
I had that problem and found it involved another google script in the project set up by a different user. The error message had a mixture of details from my script and the other script:
myFunctionName Cannot call SpreadsheetApp.getUi() from this context.
(line 2, file "OtherScript")
Line 2 of the other script did use getUi()
var app = SpreadsheetApp.getUi();
It seemed that when my script ran (triggered by onChange event) then the other script would also get run (maybe also triggered by a change event). The other script set up some UI elements, some simple menus. Normally that caused no problem. However, SpreadsheetApp.getUi() only works in the context of there being a current instance of an open editor (see https://developers.google.com/apps-script/reference/base/ui). So if a change event happened without an open editor then it fails, causing the error.
I resolved the problem by slightly modifying the other script to catch the problem:
try {
app = SpreadsheetApp.getUi();
} catch (error) {}
if (app != null) {
//Use app...
}
A different approach that might also have worked is to speak with the person and how their script was triggered, and see if they'd change it to trigger by onOpen event that just occurs when someone opens a spreadsheet and hence there is a Ui context.
So, I think your problem would be coming from SpreadsheetApp.getUi() in a different script in the project. See if the error message mentions a different file, like mine did. When your script runs in the night, there would be no context, which explains why the error occurs at that time.

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

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

How to store global values correctly using Properties service in GAS?

I have this problem using Google apps script.
I have a menu in a spreadsheet with two options (Set Password and Add Time Record). These options raise a UI service user interfaces for prompt its data respectively. For access to Add Time Record I ask before if user is authenticated. I use scriptProperties = PropertiesService.getScriptProperties() and setProperty('authenticated', false) for save the initial value of authenticated.
1- I click Set Password, I do login ok and close the UI.
2- I click Add Time Record and I expect to receive my AddTime record Ui (because I Set Password first) but instead I receive Set Password UI. Even if I login again I receive the same UI …again and again.
Is like authenticated is reset to false every time I do click in a menu option no matter the action I did before. This is the expected behavior? Or I’m doing anything wrong? Thanks a lot for your help.
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('authenticated', 'false');
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('ProWorkflow')
.addItem('Set Pasword', 'getPassword').addSeparator()
.addItem('Add Time Record', 'isAuthenticated').addSeparator()
.addToUi();
}
function isAuthenticated()
{
var scriptProperties = PropertiesService.getScriptProperties();
var value = scriptProperties.getProperty('authenticated');
if(value =='false'){
getPassword(); // at the end of this function I set scriptProperties.setProperty('authenticated', ‘true’);
}
return addTimeRecord();
}
function getPassword(e)
{
…
…
…
var scriptProperties = PropertiesService.getScriptProperties();
var value = scriptProperties.getProperty('authenticated');
value = 'true';
scriptProperties.setProperty('authenticated', value); //change de value of uthenticated
return app.close();
}
I wasn't clear on where Project Properties can be set if you want to do it programmatically. I added a function in my script:
function setScriptProperties() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('KEY', 'Some value.');
}
Then I used the UI to run this function 'manually' from the GAS interface.
After calling the function once from the UI, project properties are set.
"Is like authenticated is reset to false every time I do click in a menu option no matter the action I did before"
So it is actually.
When placing your line outside of any function it is executed on each run of ANY function so your line scriptProperties.setProperty('authenticated', 'false'); sets it to false every time.
Move it where it should be, scriptProperties and userProperties are - by definition - working as global variables since they are stored in a global script/user scope, that's actually what they are designed for..
Look also at what you wrote in the getPassword function... what you show above is not very logical (see comments in code):
var value = scriptProperties.getProperty('authenticated');// you get a value
value = 'true'; // and change it immediately to a constant... what's the point ?
scriptProperties.setProperty('authenticated', value); //change de value of uthenticated