Read Google Sheet from Google Slides getting permission error - google-apps-script

NEW INFORMATION
Nov 17, 2020 - I can run onOpen from the script without getting the permission error. I can run the function that displays data from the script or the menu (after I have run onOpen from the script). I don't think there were any changes to the code but here it is again. The manifest is definitely unchanged.
//#NotOnlyCurrentDoc
function globalVariables(){
var variables = {
// this is the Connect Four Game
sheetId: '1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA',
sheetUrl: 'https://docs.google.com/spreadsheets/d/1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA/edit?usp=sharing'
};
return variables; //return a dictionary of keys and values
}
function onOpen() {
Logger.log("In onOpen" );
var variables = globalVariables(); //load the Global variables
Logger.log("In onOpen sheetUrl from globalVariables: " + variables.sheetUrl );
try {
// var ss = SpreadsheetApp.openByUrl(variables.sheetUrl);
var ss = SpreadsheetApp.openById(variables.sheetId)
if (!ss) {
SlidesApp.getUi().alert("Spreadsheet not found!");
return;
}
} catch(e) {
SlidesApp.getUi().alert(e);
return;
}
SlidesApp.getUi()
.createMenu( 'Ask ?')
.addItem('BE1','BE1')
.addItem('BE2','BE2')
.addItem('BE3','BE3')
.addItem('BE4','BE4')
.addToUi();
prepareQuestions(ss);
Logger.log("After prepareQuestions");
setupSheets(ss); // only want to do this once
Logger.log("After setupSheets");
}
function setupSheets(ss) {
SlidesApp.getUi().alert("setupSheets() triggered");
var nextI = '1';
var cache = CacheService.getUserCache();
cache.put('nextI', nextI);
console.log("1. cache.put('nextI') = " + nextI);
SlidesApp.getUi().alert("End onOpne) = ");
}
function BE1() {
SlidesApp.getUi().alert("BE1() triggered");
var variables = globalVariables(); //load the Global variables
var sheetName = 'BE1';
var sheet = SpreadsheetApp.openById(variables.sheetId).getSheetByName('BE1');
if (!sheet) {
Logger.log("Sheet BE1 not found");
SlidesApp.getUi().alert("Sheet BE1 not found");
return;
}
getNextQ(sheet);
SlidesApp.getUi().alert("end BE1 function");
}
I am the creator of both the spreadsheet and the slides. They are in the same folder on My Drive.
I have added Oauth to the manifest in the Slide:
{
"timeZone": "America/Mexico_City",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.readonly"
]
}
I have tried to open the Spreadsheet by ID and URL. The error is:
Exception: You do not have permission to call SpreadsheetApp.openById. Required permissions: https://www.googleapis.com/auth/spreadsheets
I have tried creating a trigger (I have no idea what this is for) but when creating it the only options were time based which does not seem useful. The presence of the trigger does not change the error.
The code bound to the Slide
//#NotOnlyCurrentDoc
function globalVariables(){
var variables = {
// this is the Connect Four Game
sheetId: '1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA',
sheetUrl: 'https://docs.google.com/spreadsheets/d/1fmZCittj4ksstmhh8_t0O0csj8IDdwi9ohDDL5ZE7VA/edit?usp=sharing'
};
return variables; //return a dictionary of keys and values
}
//function installableOpen() { // never starts
function onOpen() {
var variables = globalVariables(); //load the Global variables
try {
// var ss = SpreadsheetApp.openByUrl(variables.sheetUrl);
var ss = SpreadsheetApp.openById(variables.sheetId)
if (!ss) {
SlidesApp.getUi().alert("Spreadsheet not found!");
return;
}
} catch(e) {
SlidesApp.getUi().alert(e);
return;
}
etc...
I added "//#NotOnlyCurrentDoc" as this was recommended in some places.
Share status of spreadsheet
The purpose of accessing the spreadsheet is to get data to display in html with showModalDialog. I have wandered through information on custom menus, dialogs, and sidebars to Google Docs, Sheets, and Forms, custom functions and macros, web apps, add-ons but I don't understand any of these. I was hoping to keep this simple. Custom function says it cannot display html (https://developers.google.com/apps-script/guides/sheets/functions) so that would be useless.
Any ideas appreciated.
As result of comment I changed the manifest as follows but the error is unchanged.
{
"timeZone": "America/Mexico_City",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets"
]
}
= = = = = = = = = = = = = = = = = = = =
I received this sort of email after deleting the triggers

As the error message says:
For the method
SpreadsheetApp.openById()
you are required to provide the scope
https://www.googleapis.com/auth/spreadsheets
instead of
https://www.googleapis.com/auth/spreadsheets.readonly
If you do not want to provide this scope, consider binding your script to the spreadsheet and use the method
SpreadsheetApp.getActive()
which accepts the scope
https://www.googleapis.com/auth/spreadsheets.currentonly

CRAZY FIX - I connected to the slide to which the script was bound before connecting to the spreadsheet! Pure chance discovery but the permission error is gone.
function onOpen() {
var deck = SlidesApp.getActivePresentation();
var slides = deck.getSlides();
var presLen = slides.length;
Logger.log("num slides: " + presLen );
var variables = globalVariables(); //load the Global variables
if (!variables.sheetId) {
Logger.log("SheetId not retrieved from global variables!");
return;
}
try {
var ss = SpreadsheetApp.openById(variables.sheetId);
if (!ss) {
Logger.log("The data spreadsheet does not exist");
SlidesApp.getUi().alert("The data spreadsheet does not exist" + variables.sheetId );
return;
}
} catch (e) {
Logger.log(" in catch for openBYId: " + variables.sheetId );
SlidesApp.getUi().alert(e);
return;
}

Related

getting error when opening an external Google Scripts sheet from within a Main Script

I have a Project with the following sheets within it:
KDCLog
KDCAlerts
KDCAssets
Using Google Scripts I open the "active" Log (which is expected to be "KDCLog").
function onEdit(e) {
var activeSheet = e.source.getActiveSheet();
if (activeSheet.getName() !== "KDCLog" ) return;
After this is done, I am attempting to open another (external) sheet. To do this, I followed the instructions below:
ref: https://stackoverflow.com/questions/70070131/form-data-submit-to-external-sheet-google-script
//open external sheet
var extSS = SpreadsheetApp.openById("Insert External Spreadsheet ID here");
var extSH = extSS.getSheetByName("Sheet1 External");
I followed the Message here to identify the gid (it is the one with 8 up-arrow-votes)
reference: Get Google Sheet by ID?
So what I finally wound up with is:
var extSS = SpreadsheetApp.openById(1591999114);
var extSH = extSS.getSheetByName("KDCAlerts");
But when executing the 2 statements above, I get the following message:
Error Exception: You do not have permission to call SpreadsheetApp.openById. Required permissions: https://www.googleapis.com/auth/spreadsheets
at onEdit(Code:150:32)
What am I doing wrong? Any help, hints or advice would be greatly appreciated!
TIA
#Cooper - Thanks for the response!
UPDATE 1:
I made the following change:
//https://docs.google.com/spreadsheets/d/1b5qiNH8c4wg0h1owr-P3OLRuLf-8dTBgRU9cHLBbd2A/edit#gid=1591999114
var extSS = SpreadsheetApp.openById('1b5qiNH8c4xxxxxxxx-P3OLRuLf-8dTBgRU9cHLBbd2A');
var extSH = extSS.getSheetByName("KDCAlerts");
-- AND --
I found a message that referenced this link here:
https://developers.google.com/apps-script/concepts/scopes#setting_explicit_scopes
I made the change to the appscript.json file as follows:
{
"timeZone": "America/Chicago",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "ANYONE_ANONYMOUS"
},
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.readonly",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/spreadsheets"
]
}
Still getting the error.
UPDATE 2:
Thanks for the response again. I made the changes below.
function creatTrigger() {
if(ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() == "onMyEdit").length == 0) {
ScriptApp.newTrigger("onMyEdit").forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();
}
}
function onMyEdit(e) {
var sh = e.range.getSheet();
if (sh.getName() == "KDCLog" ) {
var extSS = SpreadsheetApp.openById('1b5qiNHxxxxxxxxowr-P3OLRuLf-8dTBgRU9cHLBbd2A');
var extSH = extSS.getSheetByName("KDCAlerts");
}
else { return; }
.....
.....
.....
}
The message I am getting now is:
Script function not found: onEdit
What did I do wrong?
TIA
Try something like this:
function creatTrigger() {
if(ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() == "onMyEdit").length == 0) {
ScriptApp.newTrigger("onMyEdit").forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();
}
}
function onMyEdit(e) {
var sh = e.range.getSheet();
if (sh.getName() == "KDCLog" ) {
var extSS = SpreadsheetApp.openById("xssid");
var extSH = extSS.getSheetByName("Sheet1 External");
}
}

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

Using Google Apps Script to configure OAuth

I'm having some trouble configuring my Google Apps Script to properly handle the token that comes from the API I'm reaching out to. Everything from what I can tell is compatible.
I am using the Apps Script oAuth2 here.
When I run the below scripts I am able to get to the oAuth screen where i validate on the app, and when it passes the credentials back to google scripts on usercallback i get the below error.
Error: Error retrieving token: {"id":"401","name":"unauthorized","detail":"Unauthorized"} (line 541, file "Service")
My oAuth Script is below:
var CLIENT_ID = '...1';
var CLIENT_SECRET = '...2';
// configure the service
function getYNABService() {
return OAuth2.createService('YNAB')
.setAuthorizationBaseUrl('https://app.youneedabudget.com/oauth/authorize')
.setTokenUrl('https://api.youneedabudget.com/v1/budgets?access_token')
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('read-only')
.setGrantType('authorization_code');
}
// Logs the redict URI to register
// can also get this from File > Project Properties
function logRedirectUri() {
var service = getService();
Logger.log(service.getRedirectUri());
}
// handle the callback
function authCallback (request) {
var YNABService = getYNABService();
var isAuthorized = YNABService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
My Google Sheets script is below
// add custom menu
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('YNAB for Sheets')
.addItem('Authorize','showSidebar')
.addItem('Fetch Budget','FetchBudgets')
.addItem('Reset','reset')
.addToUi();
}
/***************************************/
// Show sidebar for Authorization
function showSidebar() {
var YNABService = getYNABService();
if (!YNABService.hasAccess()) {
var authorizationUrl = YNABService.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'Authorize. ' +
'Reopen the sidebar when the authorization is complete.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showSidebar(page);
} else {
// ...
}
}
function reset() {
getYNABService().reset();
}
function FetchBudgets() {
var YNABService = getYNABService();
var response = UrlFetchApp.fetch('https://api.youneedabudget.com/v1/budgets/default/accounts', {
headers: {
Authorization: 'Bearer ' + YNABService.getAccessToken()
}
});
// ...
}
Upon deeper investigation on this, it seems that the problem is on my fault. I mixed the URLs for client/server side auth.
https://app.youneedabudget.com/oauth/token? - this is the correct token URL.

This script does not populate sheet after parsing retrieved data

I hope this is well explained. First of all, sorry because my coding background is zero and I am just trying to "fix" a previously written script.
Problem The script does not populate sheet after parsing retrieved data if the function is triggered by timer and the sheet is not open in my browser .
The script works OK if run it manually while sheet is open.
Problem details:
When I open the sheet the cells are stuck showing "Loading" and after a short time, data is written.
Expected behavior is to get the data written no matter if I don't open the sheet.
Additional info: This is how I manually run the function
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [
{name: "Manual Push Report", functionName: "runTool"}
];
sheet.addMenu("PageSpeed Menu", entries);
}
Additional info: I set the triggers with Google Apps Script GUI See the trigger
Before posting the script code, you can see how the cells look in the sheet:
Script code
function runTool() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Results");
var rows = activeSheet.getLastRow();
for(var i=3; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 2).getValue();
var stuff = "=runCheck"
if(workingCell != ""){
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
}
}
}
// URL check //
function runCheck(Url) {
var key = "XXXX Google PageSpeed API Key";
var strategy = "desktop"
var serviceUrl = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=" + Url + "&key=" + key + "&strategy=" + strategy +"";
var array = [];
var response = UrlFetchApp.fetch(serviceUrl);
if (response.getResponseCode() == 200) {
var content = JSON.parse(response.getContentText());
if ((content != null) && (content["lighthouseResult"] != null)) {
if (content["captchaResult"]) {
var score = content["lighthouseResult"]["categories"]["performance"]["score"];
} else {
var score = "An error occured";
}
}
array.push([score,"complete"]);
Utilities.sleep(1000);
return array;
}
}
You can try the code using the sheet below with a valid Pagespeed API key.
You only need to add a Trigger and wait for it's execution while the sheet is not open in your browser
https://docs.google.com/spreadsheets/d/1ED2u3bKpS0vaJdlCwsLOrZTp5U0_T8nZkmFHVluNvKY/copy
I suggest you to change your algorithm. Instead of using a custom function to call UrlFetchApp, do that call in the function called by a time-driven trigger.
You could keep your runCheck as is, just replace
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
by
activeSheet.getRange(i, 3, 1, 2).setValues(runCheck(url));
NOTE
Custom functions are calculated when the spreadsheet is opened and when its arguments changes while the spreadsheet is open.
Related
Cache custom function result between spreadsheet opens

How to pass parameters from one Google-Apps-Script to another and execute?

Goal is to pass data from Google Apps Script A to Google Apps Script
B.
Script A is published with execute as user permissions.
Script B is published with execute as me permissions (owner).
At the very least I want to be able to pass Session.getActiveUser.getEmail() from script A to script B.
This is what I have so far...
Script A
// Script-as-app template.
function doGet() {
var app = UiApp.createApplication();
var button = app.createButton('Click Me');
app.add(button);
var handler = app.createServerHandler('myClickHandler');
button.addClickHandler(handler);
return app;
}
function myClickHandler(e) {
var url = "https://script.google.com/macros/s/AKfycbzSD3eh_SDnbA4a7VCkctHoMGK8d94SAPV2IURR3pK7_MwLXIb4/exec";
var payload = {
name : "Gene",
activeUser : Session.getActiveUser().getEmail(),
time : new Date()
};
var params = {
method : "post",
payload : payload
}
Logger.log("Hello World!");
var HTTPResponse;
try{
HTTPResponse = UrlFetchApp.fetch(url, params);
}catch(e){
Logger.log(e);
}
return HTTPResponse;
}
Script B
function doPost(e){
if(typeof e === 'undefined')
return;
var app = UiApp.createApplication();
var panel = app.add(app.createVerticalPanel());
for(var i in e.parameter){
panel.add(app.createLabel(i + ' : ' + e.parameter[i]));
Logger.log(i + ' : ' + e.parameter[i]);
}
ScriptProperties.setProperty('Donkey', 'Kong');
return app;
}
output
Going to script A here the page loads the button. Clicking the button causes "Hello World!" to be logged in Script A's project log but the log of Script B's project remains empty.
TryCatch does not log any error.
I believe your problem is due that you try to pass as a response argument a uiapp element.
here a little variation of your script in html service.
the demo
the script:
// #### Part A
function doGet(e) {
var html ="<input type='text' id='text' /><input type='button' onclick='myClick()' value='submit'>"; // a text to be passed to script B
html+="<div id='output'></div>"; // a place to display script B answer
html+="<script>";
html+="function myClick(){google.script.run.withSuccessHandler(showResults).myClickHandler(document.getElementById('text').value);}"; // handler to do the job in script A
html+="function showResults(result){document.getElementById('output').innerHTML = result;}</script>"; // function to show the result of the urlfetch (response of script B)
return HtmlService.createHtmlOutput(html);
}
function myClickHandler(text) {
var url = ScriptApp.getService().getUrl();
var payload = {
name : "Gene",
text : text,
time : new Date()
};
var params = {
method : "post",
payload : payload
}
Logger.log("text: "+text);
var HTTPResponse;
try{
HTTPResponse = UrlFetchApp.fetch(url, params);
}catch(e){
Logger.log(e);
}
return HTTPResponse.getContentText();
}
// ###### Part B
function doPost(e){
if(typeof e === 'undefined'){
return "e was empty!!";
}
var htmlOut="<ul>";
for(var i in e.parameter){
htmlOut+="<li>"+i+ " : " + e.parameter[i]+"</li>";
if(i=="text"){
htmlOut+="<li> Text hash : "+Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, e.parameter[i]))+"</li>";
}
}
htmlOut+="</ul>";
return ContentService.createTextOutput(htmlOut);
}
It is important to note that you won't have the ability to get the logger events of the script B (because it is triggered when you are not there - you are not the one who trigger script B. this is script A that's trigger script B and script A is not identified as "you" when it make a urlfetch). If you want to get the result of the script b logger you should return it to the script a.
It's important to note: again, when script A do the UrlFetch to script B it is not identified as "you" so the script B must accept to be opened by anyone (in the publish option under "Who has access to the app:" you need to select anyone even anonymous).
NB: i put everything in the same script for commodity (you can split that in two differents script it's not a problem) and because B part need to be accessed to anonymous persons I can't retrieve automatically the email adress in the part A so I changed a little bit what was done here.