It seems that if one uses SlidesApp.getActivePresentation() in AppsScript, the result of the function is not fresh but rather something that was already prepared beforehand.
Scenario
Imagine you have two users performing the following function in AppsScript simultaneously:
function updateSlideText(slideId) {
// Request exclusive write access to the document
var lock = LockService.getDocumentLock();
lock.waitLock(15000);
// Perform changes
var presentation = SlidesApp.getActivePresentation();
var textBox = presentation.getSlideById(MY_SLIDE_ID).getPageElementById(MY_TEXTBOX_ID);
textBox.asShape().getText().setText('My text');
// Save and release lock
presentation.saveAndClose();
lock.releaseLock();
}
If this function is called twice at the same time, the resulting slide contains text "My textMy text".
When I add Utilities.sleep(10000) just before the lock release, it delays the 2nd execution by 10s but after those 10s I still end up with the same result. On the other hand, if I actually delay calling the function 10s, the output is fine.
From this I conclude that it does not matter if I call saveAndClose and use locks. Once the function is called, it will always have stale data. Is there a way around this? Is it not possible to request that fresh data will be loaded after the lock is aquired?
More details
Some more pseudo-code to better illustrate the problem use-case:
// The addon frontend
websocket.onMessage((message) => {
if (message.type === 'pollUpdate') {
const slideWithPoll = store.getState().slides.find(
slide => slide.pollId === message.pollId
);
if (slideWithPoll.title !== message.poll.title) {
google.script.run.updateSlideText(slideWithPoll.id, message.poll.title);
}
}
});
I believe your goal as follows.
When 2 users are run your script for Google Slides, simultaneously, you want to run the script individually.
For this, how about this answer?
Issue and workaround:
When I tested your situation, I could confirm the same issue like My textMy text. When I tested several times, in this case, I thought that the LockService might not affect to Google Slides. So as a workaround, I would like to propose to use Web Apps as the wrapper. Because it has already been known that Web Apps can run exclusively by the LockService. The flow of this workaround is as follows.
When the script is run, the script requests to Web Apps.
At Web Apps, your script is run.
By this, even when the script is run, simultaneously, the script can be exclusively run with the LockService.
Usage:
The usage of this sample script is as follows. Please do the following flow.
1. Prepare script.
When your script is used, it becomes as follows. Please copy and paste the following script to the script editor. Please set MY_SLIDE_ID and MY_TEXTBOX_ID.
function doGet() {
// This is your script.
var presentation = SlidesApp.getActivePresentation();
var textBox = presentation.getSlideById(MY_SLIDE_ID).getPageElementById(MY_TEXTBOX_ID);
var text = textBox.asShape().getText();
text.setText('My text');
return ContentService.createTextOutput("ok");
}
// Please run this function.
function main() {
var lock = LockService.getDocumentLock();
if (lock.tryLock(10000)) {
try {
const url = "https://script.google.com/macros/s/###/exec"; // Please set the URL of Web Apps after you set the Web Apps.
const res = UrlFetchApp.fetch(url);
console.log(res.getContentText())
} catch(e) {
throw new Error(e);
} finally {
lock.releaseLock();
}
}
}
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Anyone, even anonymous" for "Who has access to the app:".
In this case, no access token is required to be request. I think that I recommend this setting for testing this workaround.
Of course, you can also use the access token. At that time, please set this to "Anyone". And please include the scope of https://www.googleapis.com/auth/drive.readonly and https://www.googleapis.com/auth/drive to the access token. These scopes are required to access to Web Apps.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
Please set the URL of https://script.google.com/macros/s/###/exec to url of above script. And please redeploy Web Apps. By this, the latest script is reflected to the Web Apps. So please be careful this.
4. Test this workaround.
Please run the function of main() by 2 users, simultaneously as you have tested. By this, it is found that the script is run exclusively. In my environment, in this case, I confirmed that even when the LockService is not used, the script is exclusively run. But I would like to recommend to use the LockService just in case.
Note:
This is a simple sample script for explaining this workaround. So when you use this, please modify it for your actual situation.
About the situation that the LockService might not affect to Google Slides, in the current stage, although I'm not sure whether this is the bug, how about reporting this to the Google issue tracker? Unfortunately, I couldn't find this issue at the current Google issue tracker.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Related
Update: I'm sorry I didn't post the code originally. In my newbie-ness, I didn't think it honestly mattered, but I've been around long enough to know better. See below:
function openForm(e)
{
populateQuestions();
}
function populateQuestions() {
var form = FormApp.getActiveForm();
var googleSheetsQuestions = getQuestionValues();
var itemsArray = form.getItems();
itemsArray.forEach(function(item){
googleSheetsQuestions[0].forEach(function(header_value, header_index) {
if(header_value == item.getTitle())
{
var choiceArray = [];
for(j = 1; j < googleSheetsQuestions.length; j++)
{
(googleSheetsQuestions[j][header_index] != '') ? choiceArray.push(googleSheetsQuestions[j][header_index]) : null;
}
item.asCheckboxItem().setChoiceValues(choiceArray);
// If using Dropdown Questions use line below instead of line above.
//item.asListItem().setChoiceValues(choiceArray);
}
});
});
}
function getQuestionValues() {
var ss= SpreadsheetApp.openById('1QeckPxMYSYGMkZ-QggY76u03N1qBKBGL2UEMcUvu7sM');
var questionSheet = ss.getSheetByName('Sheet5');
var returnData = questionSheet.getDataRange().getValues();
return returnData;
}
After months of work, I finally have cracked the script to getting what I wanted. Now I need to use this script over and over in multiple Google Forms (forms, for short). By copy and pasting the Form within Drive, I carry the App Script with each copy, but not the trigger or the authorization needed to run it. I need assistance in learning how I can make this process quicker.
I have very limited knowledge of add-ons and things, but I’m willing to learn. I do not necessarily need this to be a public access Script, just want to make my life less repetitious.
I would also like to make it so after installing the add-on, users can just pick the Sheet/Tab through a UI and not editing the code specifically, but that's a different story.
For the moment, You can create an installable trigger so you don't have to add the trigger manually anymore. You should use this:
function createFormOpenTrigger() {
const gf = FormApp.getActiveForm()
ScriptApp.newTrigger('populateQuestions')
.forForm(gf)
.onOpen()
.create();
}
Instead of:
function openForm(e)
{
populateQuestions();
}
As for the credentials, deploying the script as an add-on I will help you with the OAuth part. However, I was unable to test it today due to propagation. I’m still getting a message that the Apps Script is not bound to the GCP project. I will try my luck tomorrow, in the meantime you can read the following documentation about it. Here.
Sorry, I took some time to post the answer since you mentioned that you didn't know how to create an add-on, so I tried to add step by step information.
Update:
You can add this part at the top of the code, this will create a menu that will allow you to run the script for the first time in the Google Form after we make it an Add-on.
function onOpen() {
//you can change the names of the menu, just not change
// this part "createFormOpenTrigger"
FormApp.getUi()
.createMenu('formTest')
.addItem('Run Apps Script', 'createFormOpenTrigger')
.addToUi();
}
If you do not have a GCP project, you can created one by following the steps here, or following the steps:
Open the Google API Console projects list.
Click Create Project.
Fill out the project information for your add-on.
Click Create.
Copy the Project number, and add it to the Apps Script to switch to a different standard Cloud project.
Deploy the Apps Script as an Add-on.
Remember the version number you will need later on. Access to the Google Cloud Project link with the Apps Script.
Setup the OAuth consent screen, select internal to make sure that only you have access to it (and users of your Workspace if you have one)
We will need to the Add-on listing, so we will need to add "Google Workspace Marketplace SDK" in the Library.
Add the App configuration information, add all the require information there, the only recommendation I can provide you are these 2:
Note: this can be found under "Project Settings"
Lastly, fill out the information under "Store listing".
Note: You can add dummy information if only you are going to use the add-on.
Once all that is done, use the "App URL" link to install the add-on. When you open a Google Form it will show up under the Add-on list:
And you will see the option to Run the Apps Script, thanks to the last part of the code we added.
Click on it, and it might ask you for permission the very first time you use it. But it will not request it again even if you add it to a new form.
I'm new to GAS and I struggle with the permission system.
I'm a normal Google drive user and I started a spreadsheet and tried to add some code to it. My code is working, but only if I'm in the code editor. I want to use the onEdit() function so it's important for me that it works within the sheet as well. When I ran my code in the editor for the first time it opened a new window where I needed to enter my credentials to allow the script, then it worked. If I do some changes to a cell in my sheet and the onEdit() function is triggered I receive an error message that says something like this(translated):
Exception: You are not permitted to call UrlFetchApp.fetch. Required permission: https://www.googleapis.com/auth/script.external_request
In the editor I displayed the manifest file and added the permission to the oauthScopes but within the sheet I still receive the message. This is how my code looks like (simplified):
function onEdit(e)
{
var data = {
'key1': 'value1',
'key2': 'value2'
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(data)
};
try{
var response = UrlFetchApp.fetch('https://a-working-url.com', options); //error happening in this line
//some more data wizardry
}catch(error)
{
Browser.msgBox(error)
}
}
Any ideas how I can open this permission screen in my sheet or any hints how to solve it in a different way? I want to create a sheet with some code running in the back online. I want to share the sheet with some friends, tried it with Excel and VBA before until I realized that it's not working with Excel Online, so I switched to GAS.
onEdit(), like all simple triggers, is bound by the following restrictions (see official documentation):
The script must be bound to a Google Sheets, Slides, Docs, or Forms file, or else be an add-on that extends one of those
applications.
They do not run if a file is opened in read-only (view or comment) mode.
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause
the spreadsheet's onEdit trigger to run.
They cannot access services that require authorization. For example, a simple trigger cannot send an email because the Gmail
service requires authorization, but a simple trigger can translate
a phrase with the Language service, which is anonymous.
They can modify the file they are bound to, but cannot access other files because that would require authorization.
They may or may not be able to determine the identity of the current user, depending on a complex set of security restrictions.
They cannot run for longer than 30 seconds.
In certain circumstances, editor add-ons run their onOpen(e) and onEdit(e) simple triggers in a no-authorization mode that presents some additional complications. For more information, see the guide
to the add-on authorization lifecycle.
Simple triggers are subject to Apps Script trigger quota limits.
The ones highlighted in bold apply to your question.
Basically, it boils down to this - UrlFetchApp.fetch() is a service that requires authorization, so you won't be able to execute it from your onEdit(e) trigger, even if you have its associated scope set in your manifest file.
Use installable trigger instead and write your own "onEdit" function (with a different name) that you bind to your installable trigger.
https://developers.google.com/apps-script/guides/triggers/installable#g_suite_application_triggers
This solved the issue for me.
Hi I have a shared google sheet that enters date and time when a button is push. However, once enter I do not want the user to be able to modify the date. I know I can protect a range of cells but in this case I still need the macro/script to be able to enter the time.
Is this possible?
thanks.
I believe your goal as follows.
There are you (owner of Spreadsheet) and users.
Users run the script by clicking a button and the script puts the date value to a cell. At this time, you want to have already protected the cells for putting the date value from the users.
But, when the users click the button, you want to put the date values to the protected cells.
In your situation, Google Apps Script can be used.
From your replying, I could confirm my understanding is correct.
Issue and workaround:
When the script is run by clicking a button on Google Spreadsheet, the script is run as the user who clicked the button. So the authorization for scopes is required to be done as the users. I thought that this might be the answer of your replying I'm confused because would the script have the same permission as the person clicking the button?.
Under this situation, when the script puts a value to the cell protected by the owner, an error like You are trying to edit a protected cell or object. Please contact the spreadsheet owner to remove protection if you need to edit. occurs. So in order to avoid this error, it is considered that when the script is run as the owner, the issue will be avoided.
In this answer, I would like to propose a workaround. This workaround is as follows.
When the user runs the script by clicking a button on Spreadsheet, it runs the script as the owner by using Web Apps. When this workaround is used, please do the following flow.
Usage:
1. Prepare Spreadsheet.
Please create new Spreadsheet and create a button and assign the function of runWithWorkaround to the button. And, please protect the cell "A1" as the user of only owner. In this sample, the target cell is "A1".
2. Prepare script.
Please copy and paste the following script to the script editor of Google Spreadsheet. And, please set the sheet name which has the button.
// This function puts a date to cell "A1".
function putValue() {
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1").getRange("A1").setValue(new Date());
}
// This function is for Web Apps.
function doGet() {
putValue();
return ContentService.createTextOutput();
}
// This function is used for testing "without using this workaround.".
function runWithoutWorkaround() {
putValue();
}
// This function is used for testing "with using this workaround.".
function runWithWorkaround() {
const url = "https://script.google.com/macros/s/###/exec"; // <--- Please replace this URL with your Web Apps URL.
UrlFetchApp.fetch(url, {headers: {authorization: `Bearer ${ScriptApp.getOAuthToken()}`}});
// DriveApp.getFiles() // This comment line is used for automatically detecting the scope "https://www.googleapis.com/auth/drive.readonly" for using Web Apps.
}
3. Deploy Web Apps.
The detail information can be seen at the official document.
On the script editor, at the top right of the script editor, please click "click Deploy" -> "New deployment".
Please click "Select type" -> "Web App".
Please input the information about the Web App in the fields under "Deployment configuration".
Please select "Me" for "Execute as".
This is the important of this workaround.
Please select "Anyone with Google account " for "Who has access".
In this case, the user is required to use the access token for requesting to Web Apps.
Please click "Deploy" button.
When "The Web App requires you to authorize access to your data" is shown, please click "Authorize access".
Automatically open a dialog box of "Authorization required".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Copy the URL of Web App. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
Copy and paste the retrieved Web Apps URL to the above script.
Because the script of Web Apps is modified, please redeploy the Web Apps as new version. By this, the latest script is reflected to the Web Apps.
4. Testing.
When above workaround is used, the following result is obtained.
Note:
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
When several users use the button simultaneously, to use the lock service might be suitable. When the lock service is used, the function runWithWorkaround is as follows.
function runWithWorkaround() {
var lock = LockService.getDocumentLock();
if (lock.tryLock(10000)) {
try {
const url = "https://script.google.com/macros/s/###/exec";
UrlFetchApp.fetch(url, {headers: {authorization: `Bearer ${ScriptApp.getOAuthToken()}`}});
} catch(e) {
throw new Error(e);
} finally {
lock.releaseLock();
}
}
// DriveApp.getFiles() // This comment line is used for automatically detecting the scope for using Web Apps.
}
Above sample script is the simple sample script for explaining the methodology of this workaround. Please be careful this. So if you use this workaround, please modify your actual script using this workaround.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
I've used tabletop.js [1] in the past and is amazing! You can simply do anything you want seriously.
The only problem I saw is that you need to publish your spreadsheets to the web, which of course is really risky if you are working with sensitive data.
I'm in need now of using it in a project with sensitive data, so I was hoping someone can guide me on how to use it with spreadsheets that are not published to the web.
I've been searching for this for a long time without any success but seems that tabletop.js does support private sheets (here's the pull request that added this option [2]).
In fact, looking at the documentation they included it [1]:
authkey
authkey is the authorization key for private sheet support.
ASK: How am I suppose to use the authkey? can someone provide me with an example so I can try?
Thanks in advance!
[1] https://github.com/jsoma/tabletop
[2] https://github.com/jsoma/tabletop/pull/64
How about this answer?
Issue and workaround:
At "tabletop.js", from the endpoint (https://spreadsheets.google.com/feeds/list/###/###/private/values?alt=json) of request, it seems that "tabletop.js" uses Sheets API v3. And when authkey is used, oauth_token=authkey is added to the query parameter. In this case, unfortunately, it seems that the private Spreadsheet cannot be accessed with it. From this situation, unfortunately, I thought that in the current stage, "tabletop.js" might not be able to use the private Spreadsheet. But I'm not sure whether this might be resolved in the future update. Of course, it seems that the web-published Spreadsheet can be accessed using this library.
So, in this answer, I would like to propose the workaround for retrieving the values from Spreadsheet as the JSON object.
Pattern 1:
In this pattern, Google Apps Script is used. With Google Apps Script, the private Spreadsheet can be easily accessed.
Sample script:
When you use this script, please copy and paste it to the script editor and run the function myFunction.
function myFunction() {
const spreadsheetId = "###"; // Please set the Spreadsheet ID.
const sheetName = "Sheet1"; // Please set the sheet name.
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
const values = sheet.getDataRange().getValues();
const header = values.shift();
const object = values.map(r => r.reduce((o, c, j) => Object.assign(o, {[header[j]]: c}), {}));
console.log(object) // Here, you can see the JSON object from Spreadsheet.
}
I thought that this might be the simple way.
Pattern 2:
In this pattern, the Web Apps created by Google Apps Script is used. When the Web Apps is used, the private Spreadsheet can be easily accessed. Because the Web Apps is created with Google Apps Script. In this case, you can access to the Web Apps from outside by logging in to Google account. And, the JSON object can be retrieved in HTML and Javascript.
Usage:
Please do the following flow.
1. Create new project of Google Apps Script.
Sample script of Web Apps is a Google Apps Script. So please create a project of Google Apps Script. In order to use Document service, in this case, Web Apps is used as the wrapper.
If you want to directly create it, please access to https://script.new/. In this case, if you are not logged in Google, the log in screen is opened. So please log in to Google. By this, the script editor of Google Apps Script is opened.
2. Prepare script.
Please copy and paste the following script (Google Apps Script) to the script editor. This script is for the Web Apps.
Google Apps Script side: Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("index");
}
function getObjectFromSpreadsheet(spreadsheetId, sheetName) {
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
const values = sheet.getDataRange().getValues();
const header = values.shift();
const object = values.map(r => r.reduce((o, c, j) => Object.assign(o, {[header[j]]: c}), {}));
return object;
}
HTML&Javascript side: index.html
<script>
const spreadsheetId = "###"; // Please set the Spreadsheet ID.
const sheetName = "Sheet1"; // Please set the sheet name.
google.script.run.withSuccessHandler(sample).getObjectFromSpreadsheet(spreadsheetId, sheetName);
function sample(object) {
console.log(object);
}
</script>
spreadsheetId and sheetName are given from Javascript side to Google Apps Script side. From this situation, in this case, getObjectFromSpreadsheet might be instead of "tabletop.js".
3. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Only myself" for "Who has access to the app:".
In this case, in order to access to the Web Apps, it is required to login to Google account. From your situation, I thought that this might be useful.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
4. Run the function using Web Apps.
You can test above scripts as follows.
Login to Google account.
Access to the URL of Web Apps like https://script.google.com/macros/s/###/exec using your browser.
By this, you can see the retrieved JSON object at the console.
Note:
When you modified the script of Web Apps, please redeploy the Web Apps as new version. By this, the latest script is reflected to the Web Apps. Please be careful this.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
I am just getting started with Google Apps Script and I want to be able to add to a Google Sheet through a web app, but I'm having issues. This is what I have so far:
function addRow(input) {
var sheet = SpreadsheetApp.openByUrl('https://spreadsheeturl');
sheet.appendRow([input]);
}
function doGet(e) {
var params = JSON.stringify(e);
addRow(params);
return HtmlService.createHtmlOutput(params);
}
When I type in the URL they give me with the parameters I want to add to the spreadsheet it doesn't add anything to the spreadsheet, but if I don't pass any parameters it adds it to the spreadsheet.
For example, https://script.google.com/.../exec adds {"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{}} to the spreadsheet, but https://script.google.com/.../exec?user=jsmith doesn't add anything to the spreadsheet.
Reading the documentation I can see something about URL parameters and event objects, but they give no further information. What's the problem and how can I fix it?
Thanks,
Tomi
I think that your script works. In your script,
When https://script.google.com/.../exec is requested, {"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{}} is added to the last row at the 1st sheet of the Spreadsheet.
When https://script.google.com/.../exec?user=jsmith is requested, {"parameter":{"user":"jsmith"},"contextPath":"","contentLength":-1,"queryString":"user=jsmith","parameters":{"user":["jsmith"]}} is added to the last row at the 1st sheet of the Spreadsheet.
So can you confirm the following points on your script editor again?
At Publish -> Deploy as web app, redeploy as a new version for Project version:.
When the scripts for Web Apps was modified, the modified script is required to be redeployed as a new version. By this, the modified script is reflected to Web Apps.
At Publish -> Deploy as web app, it confirms whether Execute the app as: and Who has access to the app: are "me" and "Anyone, even anonymous", respectively.
If I misunderstand your question, I'm sorry.