I've created a custom function in a Google Spreadsheet that populates a cell with a pipe-separated list of file paths based on a loop, a prefix, postfix and some cell references. This works great on the sheet itself.
But I've also published this sheet as a CSV using the "Publish to the web..." functionality. This also works, as long as I have the sheet itself open in my browser.
But once I close that browser window, after about 5 minutes (the publish refresh window I believe), those cells are populated with "#NAME?" in the downloaded CSV. It would seem the custom functions cannot be run in the "headless" mode of a published CSV file.
Is there a way around this?
Custom functions do not provide values to the published view as they run under the scope of the person accessing. For the published view, that is an anonymous visitor with no access to the script resources (hence the #NAME error).
The solution is to implement static value assignment from script functions. Depending on your spreadsheet usage (you provide no definitive information), an on edit or a time-based trigger may be used to invoke the customized calculations.
I suspect you will want a function that is triggered when the cells containing the file paths are edited, and writes to a given static range.
function onEdit(e) {
// Ensure the edited cell was the correct sheet and column.
var s = e.range.getSheet();
if(s.getName() == ... && e.range.getColumn() == ... ) {
// Your code called from here
}
}
Related
I am writing apps script of A spread sheet for my custom functions and trying to get values in B spread sheet from there using openUrl()
However, I got ERROR in A spreadsheet when I use the custom function..
in Google Document, it says
If your custom function throws the error message You do not have permission to call X service., the service requires user authorization and thus cannot be used in a custom function.
ref: https://developers.google.com/apps-script/guides/sheets/functions
However, it doesn't say anything how to get the permission..
I tried with sharable link to everyone but it didn't work.
I tried with url&scope=https://www.googleapis.com/auth/spreadsheets
Both way didn't work. How can I solve this problem?
Unfortunately, what you're asking for cannot be done directly. In the same documentation, please scroll all the way down to sharing; that's -
Custom Functions in Google Sheets > Advanced > Sharing.
Here, you'll see the following -
Custom functions start out bound to the spreadsheet they were created in. This means that a custom function written in one spreadsheet can't be used in other spreadsheets unless...
If it suits you, you can make use of the 3 methods that they've listed there to overcome this problem.
Hope this helps!
Here's how I solved:
You can't access other spreadsheet files, which means you can't use openById() or openByUrl(). So, there is only one method you can achieve this - using getActiveSpreadSheet() which is the current spreadsheet that the app script belongs to.
If you wanted to hide the original file, then you can consider making a reference file that is sharable for anyone. And your original file is private.
So, You need to make it one more file to share.
original file (private)
Use getactiveSpreadSheet() instead of openById() or openByUrl(). And implement your custom functions. You won't have any problem with accessing the current spreadsheet in this way.
function YOUR_CUSTOM_FUNCTION(val1, val2, ...){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("YOUR_SHEET_NAME");
return getMyValue(val1, val2, ...);
}
function getMyValue(val1, val2, ...){
var result = 0;
// TODO: calculate values as you want.
return result;
}
Use your custom function in the file like this:
=YOUR_CUSTOM_FUNCTION(E5, F6, ...)
You won't get any ERROR.
reference file (sharable)
You can make a reference sheet with using:
=IMPORTRANGE("spread sheet link","sheet_name!range")
This will show access button to get authorization. Once you click, it will make a reference from your original sheet.
*range is something like A:C or A1:Z55.
We are building a google sheets database where each user has their own spreadsheet that accesses a central sheet for information using apps script.
This means that with 50 employees, we have 50 spreadsheets to maintain. I am trying to find a way to push updates to all 50 spreadsheets without having to update each one manually. I have all the apps script code in a library that each user's sheet references, so I have the coding maintenance figured out. But keeping each users actual spreadsheet up to date with the latest features is proving difficult.
One way I'm figuring to do that is have a "Template" user sheet that gets updated with the changes/new features. Then when each user opens their spreadsheet, it cross references all of its sheets to the template sheet, and checks if it needs to replace it's sheet with the latest sheet based on time that it was updated in the template sheet. For example, when the sheet "Project Report" in the template is newer than the "Project Report" sheet in the user's spreadsheet, the user SS deletes it's current "Project Report" and copies the template "Project Report" sheet to it's own via the copyTo() method.
I have this all working with apps script, but the issue now is that when the user's local sheet is deleted and replaced with the new updated seet, all formula references to that sheet in other sheets break and replace the reference with #REF. I had planned on overcoming this by using only Named Ranges, but even the named ranges break when the sheet is replaced to the point where even the apps script can no longer find the named range because the named range it is looking for was automatically renamed when the new version of the sheet was imported (aka, "CustomNamedRange" in the template SS was renamed to "'SheetName'!CustomNamedRange" in the user SS).
The only way I know to overcome this issue at this point is to create a centralized "Range Index" spreadsheet that has all the named ranges with their destination sheet and range. I would have to create a custom function that filters through the range index and finds the address it needs based on the name given. For example, instead of calling "CustomNamedRange" in a sheet formula, I would call custom function: getNamedRange("CustomNamedRange"), and apps script would return the range found in the range index. And when a sheet is replaced with the newer version, no references would break because all references go through the apps script filter function.
The only problem with this is that I can foresee this method (calling every range needed in the script through a custom function) slowing down my spreadsheet A LOT because every time a range is called for, it will have to go search through the range index to find it and return it.
Does anyone have any other ideas on how to accomplish what I'm looking for? As in keeping 50+ individual spreadsheets updated with new features without having to do it manually and without breaking all the references?
Sorry for the long post, but I appreciate any ideas!
I had a similar problem and was able to resolve it by using SheetAPI to replace text. I have a template called Sheet1_Template and its hidden. I delete Sheet1, copy Sheet1_Template, show it and then replace all occurances of "Sheet1" in formulas to "Sheet1". Sheet API has to be enabled in the Resources and Google API Console.
function copyTemplate() {
try {
var spread = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spread.getSheetByName("Sheet1");
if( sheet !== null ) spread.deleteSheet(sheet);
sheet = spread.getSheetByName("Sheet1_Template");
sheet = sheet.copyTo(spread);
sheet.setName("Sheet1");
sheet.showSheet();
sheet.activate();
spread.moveActiveSheet(0);
var requests = {"requests":[{"findReplace":{"allSheets":true,"find":"Sheet1","replacement":"Sheet1","includeFormulas":true}}]};
Sheets.Spreadsheets.batchUpdate(requests, spread.getId());
}
catch(err) {
Logger.log("error in copyTemplate: "+err);
}
}
I haven't been able to test implementation of it yet, but I believe the answer above is what I was originally looking for.
I haven't spent any time messing with the API yet, so in the meantime I have found another solution:
Google Sheets recently added macros to it's feature set. The beauty of this is that You can see and edit the macro code after you've recorded your actions in the sheet. For now, I plan on recording a macro when I make updates to the template sheet, then copying the script for that macro into a custom function in my library that will run every time a user opens their spreadsheet. When they open their SS, apps script will check to see if the library's macro function has a later date than the last time the sheet was opened. If it does have a new date, then it will run the macro script, and that user's SS should get updated to the same state as the template.
Also if you are seeing that you cannot run the query from #TheWizEd
It may be due to "Sheets API" not being enabled at Advanced Google services. Please enable>
In the script editor, select Resources > Advanced Google services In the dialog that appears, click the on/off switch for Google Sheets API v4. Please turn on. Click OK button.
Thank you so much to TheWizEd for getting me started (please vote for that post too).
This is what I needed:
function replaceFormulasInSheet(sheet, searchFor, replaceWith) {
// https://stackoverflow.com/a/67151030/470749
// First you need to do this to enable the feature: https://developers.google.com/apps-script/guides/services/advanced#enabling_advanced_services
// https://developers.google.com/sheets/api/quickstart/apps-script
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#findreplacerequest
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate
const spread = SpreadsheetApp.getActiveSpreadsheet();
const requests = {
"requests": [
{
"findReplace": {
// "allSheets": true, Omitting this property and instead setting the sheetId property is the only way to effectively set allSheets as false.
"sheetId": sheet.getSheetId(),
"find": searchFor,
"replacement": replaceWith,
"includeFormulas": true
}
}
]
};
return Sheets.Spreadsheets.batchUpdate(requests, spread.getId());
}
Also note that it does not work for sheets with hyphens in their names. If you need hyphens in their names, remove the hyphens beforehand and re-add them after.
I have a spreadsheet where I want users to enter information freely then "submit" their data. Once they "submit" this data I would like to protect a specific range where all other users cannot edit any data within that range with the exception of the owner; but still have the ability to view and access this data for reference.
I have tried this a couple ways and keep running into various obstacles. I am not very familiar with google script but am finding this is the only way to accomplish this automatically with little maintenance. Doing so, I have ran into two main obstacles:
The first one being the script that is being run is protecting the range based on a cell value. If the cell value of N7 is "Approved" it is supposed to protect the range. What is happening is the script is protecting the range regardless of what the cell value is (the value can be blank or a different word and it will still protect the range).
The second issue is, if I have the script run off of a "button" or recorded macro, the script will not remove the editor clicking the button. So with this issue, I (the owner) would need to go in every time to click the button to remove all editors.
The current script that I have right now is listed below. This script is to perform the protection based off the cell value in N7 on open:
function onOpen() {
var spreadsheet = SpreadsheetApp.getActive();
var approvedCellD1 = spreadsheet.getRange('N7').getValue();
var rangeD1 = spreadsheet.getRange('B5:L64');
var protectionD1 = rangeD1.protect().setDescription('Day 1 Approved');
if(approvedCellD1 == "Approved") {
protectionD1.removeEditors(protectionD1.getEditors());
protectionD1.addEditor(['myemail#gmail.com']);
if (protectionD1.canDomainEdit()) {
protectionD1.setDomainEdit(false);
}
}
};
I made a similar version of the spreadsheet it with the main components and still receive the same issues.
https://docs.google.com/spreadsheets/d/19NReUL2GSNWjMGrzvskQx86JDWox3e2wzDpk3PnKGX4/edit?usp=sharing
Please let me know if you have any suggestions or advice that I can try!! I have been searching and modifying my scripts for almost 2 months now and am still experiencing difficulties.
THANK YOU!!!!
I'm trying to figure out how I can write and run a custom function in a Google spreadsheet and I've been following this google tutorial.
However at the time of this posting, I fear that this tutorial is out of date. I followed the steps, but see no way to access my in2mm function from within the spreadsheet that I started at. The tutorial suggests the following to make it show up
You can manually start the scan by going to Tools > Script Manager... and clicking the Reload button.
However, no such menu item exists.
Just cut off in that screenshot is a "Script Center Menu", which has just one option for "Read Data". It's unclear what that does. It's also unclear how the "Script Editor" ends up tying back into the existing spreadsheet to become available...
Does anyone know the current steps required to write a simple google script function and then access it from within a Google spreadsheet?
I had the same problem, and while the tutorial and Sum Ting Wong's answer actually do work, it didn't help in my case.
The sheet I was trying to use the custom function from was in the old format. So I converted it to the new format,created a custom function, and now I can use it the sheet.
Here's how you see if it's an old format sheet Check out the new Google Sheets
You can tell that a spreadsheet has been created in, or upgraded to, the new Google Sheets if it has a green checkmark at the bottom.
and here's how to convert it to the new format:
Moving spreadsheets to the new Google Sheets
you can manually move spreadsheet contents into the new version of Sheets to take advantage of new functionality, following any of these steps:
Copy and paste content from a spreadsheet created in the old version to a spreadsheet created in the new version.
In a spreadsheet created in the old version, click the down arrow next to a sheet tab and click Copy to…, and copy the sheet (and its contents) to a spreadsheet created in the new version.
Export the contents from the old version and import them into a spreadsheet created in the new version.
forget the reload button hint.
if you have in the first step write your function in the script editor and save it.
function in2mm(inNum) { // Function to convert from INCHES to MILLIMETERS
var outNum = 0; // this will hold the answer
var factor = 25.4; // multiply input by this factor to get output
if (typeof inNum != "number") { // check to make sure input is a number
throw "input must be a number"; // throw an exception with the error message
}
outNum = inNum * factor; // calculate the answer
return outNum; // return the answer to the cell which has the formula
}
for your second step write e.g. in cell A1 of your sheet, to call the function
=in2mm(10)
important is that you call your function-name started with the equal sign =
if you do a type-mismatch by your second step you get the message
#NAME?
there is no mystic and no out of date ;-) btw i imagine they talk from the browser reload button
Custom Function still work in google sheets.
function GETKWEEKDAYFROMNBR(weekdayNbr) {
var weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
return weekdays[weekdayNbr];
}
usage example:
=GETKWEEKDAYFROMNBR(WEEKDAY(A2,2))
I've got the same problem. I've had a spreadsheet open for a few days and under Tools there are three script options, those being Script Gallery, Script Manager and Script Editor.
I started a new sheet and went to the script editor, and there's only two options available, just like in your image. If I select Script Gallery I get this message;
Script gallery is now the add-on store
In the new Google Sheets, the script gallery has been replaced with the add-on store. Click the new Add-ons menu to get started. Learn more
The only solution I can see to get the script to work is by running it from within the script editor itself.
I have created a form that pushes data to a Google Spreadsheet. The data is latitude, longitude, location, and other identifying data. The spreadsheet is then published as a .CSV file and imported into ARC GIS to be displayed on an interactive map. It works exactly as I wanted and I set it to republish after each change.
The problem is that when the spreadsheet has rows appended by the script, it is not seeing it as a change and republishing. In order to get the updated data imported to the map, I need to go in and manually republish. Is there anyway through the Google Apps Script that I could make a few lines of code to force a republish? I could then add that to the "on form submit" script I have or another time based one that already runs at 3 am everyday.
I have looked through the Google Apps Script documents and not found anything. When searching for help on the web, the overwhelming majority of responses are for how to publish your script as a template for other.
My testing sheet was republished after the following function was executed by either a menu entry or a time-based trigger.
function ChangeIt() {
var sheet = SpreadsheetApp.getActiveSpreadsheet()
var t = new Date()
var x = 'upd: ' + t
var range = sheet.getRange('a3')
range.setValue(x)
}
If I were in your shoes, I'd add an extra column to the end of the sheet with some benign constant data that a script can change without affecting the systems consuming the data. If an extra column isn't an option, try modifying my sample to read in a current value, change it, and immediately change it back.
Also, I'd see if the spreadsheet onEdit() trigger fires when the form submit adds a new row. If so, tie your GAS function to it to force the republish. If not, setup a timed trigger to execute the GAS function.
A quick workaround for this issue that doesn't require scripting is to simply make an array copy of the data.
For example, I made a new tab and in A1 put this: =ArrayFormula('Form Responses 1'!A1:Z1000)
While the main Form responses tab will insert rows and not play nice with formulas this new tab stay nice and constant and updates automatically when new data is added.