Centralized script for multiple spreadsheets - google-apps-script

I have created a Notification script and would like to use it for multiple spreadsheets (produced by Forms). I would like to have a centralized script and referencing it on my multiple spreadsheets in order to simplify maintenance.
I have tried to use the SpreadsheetApp.openById() like below but looks like this feature has been disabled for security reasons and is not supported anymore.
function append()
{
SpreadsheetApp.openById("1xdePF..........................");
}
Any idea on how to use the same script for multiple spreadsheets?

The documentation specifies that openById opens the spreadsheet with the given ID.
You cannot use this method to open a script.
To open an Apps Script, go to https://script.google.com/home/my and clock on the script of your choice.
For bound scripts, you open the document to which the script is bound and go to Tools->Script Editor

Preface
This answer is supplementary to ziganotschka's, since you indeed cannot access the script bound to a document by opening said document with openById(). The answer, instead, covers other issues you had and suggests additional ways to solve your task.
Problem
When trying to use openById() you receive an error message of the following structure:
Exception: Document [doc id here] is missing (perhaps it was deleted, or maybe you don't have read access?)
You mentioned in comments that the id is 58 chars (I bit it is 57) and obtained from "properties", which explains the error - there is no spreadsheet with such an id, because file id and script id you extracted are not the same thing. If you ever need to extract current id programmatically (here it is assumed to be called from a script bound to spreadsheet, but other services have similar methods), you can call getActiveSpreadsheet() -> getId().
Solutions
You stated that you need a maintenance script, so how about creating a standalone script project that is deployed in a way any document can access:
As a library
Any script that has a saved version can be a library that can be used by other scripts by adding its id to the list accessible from Resources->Libraries menu.
As a Web App
Any script that has a doGet, doPost function (or both) can be deployed as a Web App, essentially exposing it to the net. Since you said that the spreadsheets are "produced" by Forms, and you created a "notification" script, I assume you are interested in a FormSubmit event. When you deploy as a Web App, you get a url (don't forget to choose an appropriate permission).
After that, it is only a matter of making sure that:
each Form has an installable onFormSubmit trigger (you can instlall it via [please see refs for docs] ScriptApp.newTrigger('callback name').forForm('form ref').onFormSubmit()).
The callback for the trigger calls the Web App url via UrlFetchApp.fetch() with necessary data (like spreadsheet id or any other info) as query (if using doGet) or request body (if using doPost).
The data you need will be available in the event object constructed on hit to doGet or doPost [note to avoid common misconception: you can't debug event objects in the editor, it can only be done live].
Reference
getActiveSpreadsheet() docs
getId() docs
getFileById() docs
Standalone scripts guide
Bound scripts guide
Libraries guide
Form submit event reference
new Trigger() docs
UrlFetchApp.fetch() docs
doGet / doPost event object docs

Related

How can I Find All Uses of a Google Script Library?

I have a Google Script library that is used by at least 100 other scripts (some that are bound to spreadsheets/documents, some that are not). How can I find all of these client scripts that reference my library script?
More specifically, I need to be able to add a new feature into the library that requires new permissions that I (the user) must grant. The client scripts won't run if I just add this feature to the library without granting the permissions to each of the client scripts. So ultimately, I need to give this new permission to each of the clients. And if I knew what scripts were actually using this library, I could do that manually for each one. But I need to URL's or ID's or something for each of those scripts.
Answer:
Unfortunately this is not possible to do.
More Information
It is possible to get a list of standalone Scripts from your Drive, though scripts bound to a file can not be searched for using regular searching methods.
It is possible, using the help of this Google Account page to get a list of all the Apps that have access to your account, though only files you have authorised will appear here, and apps which are not just those created by you in Apps Script will appear there (for example, other add-ons or even Android Apps bound to your account appear here).
A Partial Workaround:
Using Google Apps Script, you can list all Apps Script Projects that you own with help of the MimeType enumeration GOOGLE_APPS_SCRIPT
var scripts = DriveApp.getFilesByType(MimeType.GOOGLE_APPS_SCRIPT);
var arr =[ ];
while (scripts.hasNext()) {
var script = scripts.next();
arr.push(script)
}
Logger.log(arr);
Or even just searching for type:script in Drive, however this only returns a list of scripts that are not bound to a file.
You can then use regular Google Drive search terms to find which of these files contain, for example, a unique method name that the library uses. I am aware this isn't a perfect solution and you would still have to look for projects bound to a file using the above webpage.
Feature Request:
It appears that back in 2014 a feature request for this was made on Google's Issue Tracker, though I would suggest creating another feature request for this here as it was marked as a duplicate of another issue. You can make a feature request under the correct Apps Script component here.
References:
Google Apps Script - Enum MimeType
Google Drive Search Query Terms
Apps with access to your account
Google's Issue Tracker
Feature Request: Listing and searching for container bound scripts
Create an Apps Script Feature Request

Getting all viewers of spreadsheet using App Script

i wrote code for getting all viewers of spreadsheet in app script .
i have used getViewers() method to get viewers names who actually viewed it. but that method is returning me the names of people to whom i actually shared the spreadsheet....
is there any other way that i can get all viewers of spreadsheet.?
is there any web automation tools that can solve my problem?
Answer:
It is not possible to get a list of people that have opened your a Google Drive file using Google Apps Script - a method that returns this list does not exist. The getViewers() method returns the list of people with view and comment permissions for a file, while getEditors() retrieves the list of people that have edit permissions.
The Issue:
is there any other way that i can get all viewers of spreadsheet.? is there any web automation tools that can solve my problem?
There is no way of getting viewers of a Google Sheet as this is a huge security issue. This information is not stored and is therefore not retrievable.
Workaround:
You can make a custom function which stores the username of a person when they open the file - though be aware that triggers have restrictions and will only run if the person opening the file has edit access. I have included a list of Apps Script Trigger Restrictions below for you to look through and decide what is the best approach for you.
Code:
function onOpen(e) {
var user = e.user.getEmail();
// do some code to store or save this parameter
// for example save it to a hidden sheet or email it to yourself
// though an email would require an installable trigger to be made
}
Simple Trigger Restrictions:
These are not all of the restrictions (full restrictions are available here), but these are the ones that I believe to be most relevant for you.
As per the Google Apps Script Simple Triggers documentation:
They do not run if a file is opened in read-only (view or comment) mode.
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.
This last point is important - getting information about the current user is possible depending on the security policies of the the G Suite domain. A detailed explanation of this can be found in the getActiveUser() method documentation:
If security policies do not allow access to the user's identity, User.getEmail() returns a blank string. The circumstances in which the email address is available vary: for example, the user's email address is not available in any context that allows a script to run without that user's authorization, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user). However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same G Suite domain as the user.
There are big security issues with getting a list of people that have viewed a file, for good reason, and so what you are looking to do it highly restricted by Google.
References:
Class File of Google Apps Script
getViewers() method of Class File
getEditors() method of Class File
Simple Triggers
onOpen(e) Trigger
Simple Trigger Restrictions
Installable Triggers
Event objects
Class User of Apps Script
User.getEmail() method
Class Session
getActiveUser() method of Session Class

Using Google Apps Script Libraries

I have read all Google documentation on managing and creating libraries, yet I still do not know if they are an appropriate option for the problem I am trying to solve.
I know how to save a version of a standalone script. I know how to add the library to a spreadsheet via the script editor. But I don't understand, very simply, how to trigger the library script within the new spreadsheet.
I have a spreadsheet that serves as an often-copied template within my organization. The template contains a script that (onOpen) accesses data on a separate spreadsheet (a master database) and sets those values on a tab called "admin." The desired result is to have a copy of the master database living within the template sheet (and every subsequent copy of the template sheet). At this point, there are thousands of copies of the template sheet, each running that same script.
Whenever I have to change the script, I have to change it within thousands of sheets. Can I use a library instead? I'd like to be able to create a new version of the script in the library and have all sheets connected to that library experience the change. I understand that the library needs to be in development mode (within each sheet) to do this. I also understand that in order to make this switch, I will probably still have to go into each sheet to add the library. I'm just hoping it will be the last time I have to do such a tedious task.
Any advice or links to solid info is appreciated.
besides making an add-on (already covered in another answer) I will answer your libraries question. They will work for you. What you are missing is the "connect" part.
For this you want to trigger the library code from say, onOpen. The onOpen in the library is not enough and not detected by apps script. Instead each of your spreadsheet's script needs an onOpen(e) which just calls yourlibrary.onOpen(e).
since those "hook" calls rarely change, specially once you stabilize your library api, and using it in "development" mode will let you modify just the library.
whenever one of those hooks needs to change (say a callback from an html GUI needs a new parameter) you need to update all the spreadsheets. to avoid this, make all your callbacks receive a single json object instead of multiple parameters.
Sorry if I am repeating other answers, but I would like to sum up and add something:
You can access your library functions as follows:
From the app using the library you go to the Resources/Libraries. You can see the library name under "Identifier". On the same line where you can select Development mode.
Library name found in resources
Now in your library you have for example a function
function onOpen(e)
{
Browser.msgBox("HELLO!");
}
In the spreadsheet app you wish to access it you use the library name found in the resources, for example "testlibrary"
function onOpen(e)
{
testlibrary.onOpen(e);
}
Now if you have development mode on, the modifications to functions in the library update automatically to your application (spreadsheet) as long as the user using the application has edit access in your library script.
If anyone using your spreadsheet has a restricted access to your library script (meaning only view access) or development selection is off in the application, you have to go to the application's script, Resources/Libraries and select the most recent version of your library to be used in the app everytime you update the library and save a new version of it.
Still, especially if you are using mostly only the onOpen function , I would recommend using the library rather than copy-pasting the function to the script of each spreadsheet, as it is easier to track which scripts are up to date and it is easier to avoid errors and differences between the scripts.
Even in the more restricted case, if you update function in library - as long as you are already calling it in the app - all you have to do is select the new version of the library used.
I hope I had anything to give in this conversation and my language was appropriate, this was my first answer..
A good question Melly. I've been through a bunch of the documentation and some tutorials on this subject but haven't tried adding any libraries yet. My understanding is that once you are connected to a library all you have to do is call the applicable functions. That once a library is connected the functions in the library become an extension of all the other Apps Script classes available in the script editor.

How can I invoke my standalone script in my spreadsheet?

I have created a standalone Apps Script from Google Drive, but when I try to access it from a Google Spreadsheet, I don't see a way to access the script (even though when I created the script, I created it as a spreadsheet script).
In the spreadsheet, the "Tools->Script manager..." menu item doesn't show my script, nor does the "Tools->Script editor...". The latter has a section for 'recent projects', but it doesn't list my newly created script, nor its associated project.
If I create a new script from inside the spreadsheet (i.e .Tools->Script editor...) and cut and paste the code from the standalone script, it works fine. However, this can only be used in the single spreadsheet -- to use it from another one, I have to cut and paste / go through auth again.
Is there a way to do this without publishing the script to the gallery, which I assume makes it public (I don't have a problem making it public, but it seems like there must be a better way)? The script is here -- it is world readable with the link. It adds a menu to a google spreadsheet that lets you run a query in BigQuery and dumps the results into your spreadsheet.
The behavior you describe is exactly how it is intended to work... the script you wrote can only work if it is bounded to a spreadsheet.
The fact that you wrote it in an independent script makes it indeed a standalone app but it can only be executed from within the script editor and in this context an onOpen function like the one you wrote doesn't make sense (it runs on opening a document/spreadsheet, not the script).
For now a script can only be bounded to a spreadsheet by copy/pasting the code, at least a script that you want to use as you do (with a menu and direct function calls and activeSheet calls), maybe one day Google will expand the concept of libraries to make your script function available from different document directly (read also the doc about libraries).
There are actually 2 types of standalone Apps Scripts, the one you tried that runs only from within the editor and webapps that are deployed and run in a browser window by themselves. The latter are always build around a doGet function that represents an entry point for the application. You should read the documentation on the subject to get all the details.
To summarize : if you want to access your spreadsheet using getActiveSpreadsheet() and similar methods then you have to write (or paste) the script in the script editor bounded to the spreadsheet.
If you want to have a standalone script you can also interact with spreadsheets (and other document as well) but you have to access them by their ID and the spreadsheet will only be viewable from another browser window, without link between them, and there will be no "active spreadsheet" nor "active sheet"...
A webapp can of course also do that with the advantage of a user interface that you can adapt to your needs but this is becoming a bit too far from the scope of your question.
Hoping I've made it (a bit) more clear, at least I tried to make it simple ;-)
I worked this out with Jordan, following the instructions here:
https://developers.google.com/apps-script/guide_libraries
First, he had to share his standalone script with me, so I had read access to it (probably setting the script as public read would work too).
Then he told me his project key.
I took his project key and inserted it in "Find a Library", in the "Manage Libraries" menu.
For this to work, Jordan had to export "a version" of his library - so third parties can rely on stable code and upgrade at their own rhythm.
As Jordan's script runs automatically on onOpen(), my code had to define a function onOpen() that called his script onOpen(). To do this, the library manager gave me an identifier for the imported library.
Sample code once the library is imported and the library manager gives you an identifier for the library:
function onOpen() {
GivenLibraryIdentifier.onOpen();
}
You can also use installable triggers. If you install an installable trigger (such as onOpen) from your standalone script into the document, it gets similar privileges as if it were a simple trigger.
And better yet, when the installable trigger calls its function from your standalone script, it executes WITHIN your standalone script! So it can call any other function in your standalone script. Its Logger.log() entries can even be viewed from your standalone script, even though it was executed from your sheets document.
So basically, if your script installs an onOpen() trigger tied to one of its functions, it's as good as if your Sheets document had required() your whole standalone script file.
(Assuming the owner of the script has edit privileges to the file.)

Deploying container-bound Google Apps Script as Web App

I'm creating a spellchecker using the Google Docs API in an Apps Script (just a script that extends the functionality of a Google Doc), and I wan't to make this service available to users whom download it as a Web App. Problem is that when I've made my (Container-bound) script in the script editor, it is only available in the Google Document through which I created it - that is, if i open a new document, I cannot use the script.
If I "Deploy as Web App", make it available to everyone and paste the given URL, I get an error message saying that the script needs a function called doGet(), which is not in my script.
How do I go about to publish my script as a regular web app?
I would proceed by creating two scripts:
the core functionality would be deployed as a Web App and a simple container-bound script would offer an interface to call the Web App.
Since the Web App is not bound to a document you may want to follow this scheme:
function doGet(e){
if(e.parameter.docId){
doStuff(DocumentApp.openById(e.parameter.docId));
}
}
Now when you deploy the app you will get a link that gives you access to the functionality.
From the container-bound script you can add some UI (e.g. an Anchor element in a side-panel) that links to the web app with the appropriate parameters
ScriptApp.getService().getUrl() + "?docId=" + DocumentApp.getActiveDocument().getId()
or use UrlFetchApp to get the results and display them in the UI.
Unfortunately this is not the same as adding the functionality across all your documents automatically, but rather a way to install only a relatively lightweight hook in each document where you want to add the functionality, instead of the full script.
I am not aware of any method that can achieve that. Note that when you make a copy of a document, the copy will contain all its scripts so you can create a template for documents that need the additional functionality. This can get ackward though if you wish to mix and match multiple scripts.
The advantage of my method is that if you modify the core functionality, the change is immediately available to all your documents making use of it, with no need to update their scripts. On the other hand if the container-script needs to interact heavily with the UI it may get complicate and reduce the usefulness of separating it in two scripts.
The answer is in your question : the main function of your script (the one that builds the UI) must be called doGet() (this is the conventional entry point of any GAS webapp, the function that you implicitly call when opening the webapp url)... but I'm afraid this will not solve your problem...
Even if I don't know what is in your script and how you wrote it I guess that it refers to the doc in which you bound it as the "active document" and that will probably be the most important issue since a webapp has no document attached to it.
Beside that, how would a spell checker work as a standalone app ? could you explain more clearly what you want to do ?