Google Apps Script - Editors Execute Script on Spreadsheet - google-apps-script

I created a Google spreadsheet to help my team keep track of their calendar for the upcoming week. The spreadsheet has 2 tabs: an "Overview" tab and a "Details" tab. On the "Details" tab, the top row lists all of the weekdays through the end of the year and the left column lists the team members names. For each day, the team member writes what school they will be at and the description of support.
The "Overview" tab has the same structure as the "Details" tab (i.e., dates across the top and names listed on the left), but provides a visual summary of where each person will be. I wrote a script that takes the school name from the "Details" tab and enters it in the cell that corresponds to the correct person/date on the "Overview" tab. Then it takes the description of support and creates a Note on the "Overview" tab with the description. It also shades the cell on the Overview tab so I can see that support is planned. So from the "Overview" tab, all I have to do is mouse over the cell and it will show me the planned support.
It works great in my own account. However, I have shared the spreadsheet with some team members and it is not working. I don't want my team to be able to edit the "Overview" tab because I only want them to input their information on the "Details" tab. If I give them editing rights to the "Overview" tab, then they can change the Notes, cell colors, sheet structure, etc (that is why I want the Overview tab as view-only). So, I protected the "Overview" tab so they cannot make edits. However, because they cannot edit this sheet, it seems that the script will not run and update the Overview tab when they edit the Details tab. I assume this is because they don't have permission (because the sheet is protected). When I remove the sheet protection, the script runs just fine for them.
Any thoughts on how I can get around this? I really need to keep the Overview tab View-only. Thanks.

If, as you stated in comments, you use an installable on Edit trigger the problem you describe should not occur since "When a script runs because of a trigger, the script runs using the identity of the person who installed the trigger, not the identity of the user whose action triggered the event. This is for security reasons. For example, if you install a trigger in your script, but your coworker performs the action that triggers the event, the script runs under your identity, not your coworker's identity. For complete information on this, see Understanding Permissions and Script Execution."
see doc here
EDIT : sorry, I didn't see the issue about this special case : issue 1562 posted on july 2012, status "triaged"
EDIT 2 : I tried #tracon6 suggestion to remove the protection temporarily but it doesn't work either... the script generates an error when trying to apply the protection.
but
as an EDIT 3
I found a workaround that works ! We can add an editor just for the time we write to the targetSheet and remove it right after... using a flush in between it works.
here is the code I used to test :
function onEditInstallable(event) {
var sheet = event.source.getActiveSheet();
if(sheet.getName()=='Sheet1'){return};
var r = event.source.getActiveRange();
var column = r.getColumn();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var targetSheet = ss.getSheetByName("TFA");
var targetSheet = ss.getSheetByName("Sheet1");
var user = SpreadsheetApp.getActive().getEditors()[1];
var permission = targetSheet.getSheetProtection();
permission.addUser(user);
targetSheet.setSheetProtection(permission);
SpreadsheetApp.flush();
targetSheet.getRange(1,column).setValue('value change on sheet 2, column : '+column);
SpreadsheetApp.flush();
permission.removeUser(user)
targetSheet.setSheetProtection(permission)
}
EDIT 4 :
Since it seems that many editors could have access to these sheets (see last comments), there has to be a way to know who is active on the spreadsheet. In a non domain environment this is not possible with a triggered function so I would suggest a small uiApp with a list from which the user has to choose, trigger that with an installable onOpen and store the value to use it in the main function.
Heres is a piece of code to handle that aspect.
function SpecialonOpen(){
var s = SpreadsheetApp.getActive();
var app = UiApp.createApplication().setTitle('Please select your email in this list').setWidth(300).setHeight(300);
var list = app.createListBox().setName('list')
var editors = s.getEditors();
for(var n in editors){
list.addItem(editors[n].getEmail());
}
var handler = app.createServerHandler('getMail').addCallbackElement(list);
var btn = app.createButton('select',handler);
s.show(app.add(list).add(btn))
}
function getMail(e){
var email = e.parameter.list;
var editors = ScriptProperties.getProperty('currentEditors')||' ';
if(editors.indexOf(email)==-1){
editors+=(','+email);
ScriptProperties.setProperty('currentEditors',editors);
}
var app = UiApp.getActiveApplication().close();
var editors = ScriptProperties.getProperty('currentEditors').split(',');
if(editors[0].length<2){editors.splice(0,1)};
Logger.log(editors)
return app
}
You should also add a function to reset this list at some moment... don't know what would be best ? on a daily base maybe ? I'll let this to you ;-)

Do the Overview tab and the Details tab need to be in the same spreadsheet?
Perhaps you can have the Overview tab in a spreadsheet that only you can access and have the Details tab in a spreadsheet that everyone else can access -- then you can modify your script to use the information in the new "Details Spreadsheet" to update the new "Overview Spreadsheet".
Alternatively, in your script, you could turn off the protection on the Overview tab, then run the main part of the script, then turn on the protection on the Overview tab again before the script ends (see https://developers.google.com/apps-script/reference/spreadsheet/page-protection#setProtected(Boolean))
Admittedly, neither of these options are as clean the onEdit option.

Here is a hacky workarround you can try: publish the script as a content service with anonymous access, and call it with urlFetch from your trigger, passing the necessary param to your service.
This should cause the code to execute in a different context where the effective user is the script publisher and not te user. Will be slower thou.

Why don't you protect the Overview sheet using the options in the Data tab and set permissions for edit to 'Only you'. This will protect the sheet from everyone, even editors, leaving the sheet in View Only mode for everyone except yourself.

Related

Only allow users to edit a specific range whilst using a 'view only' type share

I am running a sheet which people can copy through running a script, on the template file I want users to only see one of the sheets which contains a button that runs a script to copy the file.
I really want to share the document as a 'View Only' file although I still want the users to edit the range where the button is so that they can click on this and run the script.
I have tried protecting sheets and removing editors through Apps Script by using the class protection but this doesn't seem to work - I created the below which I had hoped would work:
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.getSheetByName('BALANCE SHEET').protect();
ss.getSheetByName('LANDING SHEET').protect();
ss.getSheetByName('CATEGORIES').protect();
ss.getSheetByName('REMINDERS').protect();
var user = ss.getEditors();
ss.removeEditor(user);
ss.addEditor('xxx#xxx.xxx');
does anyone have an idea of how I can do this please?
Spreadsheet.getEditors() returns a array of users, whereas removeEditor accepts a user(singular) as a argument. Use Array.forEach to remove users:
const users = ss.getEditors();
users.forEach(user => ss.removeEditor(user));

Google Sheets - Replace sheet without breaking references to that sheet

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.

How to fire an onOpen event in Google Spreadsheet when a spreadsheet was 'Make a Copy'?

I know some basic javascript/app-script and i have my following code
function onTimesheetOpen() {
var source = {
ssId : '1Mqbh09mj_UoRZiQkzw1gOVpHFiYk-8qZvpnDbzCYOFY',
shName : 'prompt',
a1Notation : "A1:A100",
};
var target = {
ssId : SpreadsheetApp.getActiveSpreadsheet().getId(),
shName : 'Setups',
a1Notation : "A1:A100",
};
AsExt.UTLCopyCell(source, target);
}
It's a READONLY timesheet template shared to a group of workers, where it was intended to copy list of employees from employee sheet (source) and populate into my target (the template sheet). The onTimesheetOpen() trigger because i set a trigger "From Spreadsheet", "On Open" event.
Everytime the template opened, it will copy updated employees from master employee sheet.
The workflow for the workers are to 'make a copy' from the template timesheet file to their drive (My Drive) and i noticed the trigger will be deleted everytime i made a copy. It's quite different from traditional programming language and i do hope to get some helps from the gurus :)
I couldn't expect every worker to open script editor after 'made a copy' as they are not IT trained, their jobs is to get the timesheet, which pull for a list of latest employees (master file).
I also tried the installable trigger
https://developers.google.com/apps-script/guides/triggers/installable
which is also not that suitable.
In order to create a trigger into their copied file, they have to run following code ONCE (which are confusing as they not IT trained)
function createSpreadsheetOpenTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('onTimesheetOpen')
.forSpreadsheet(ss)
.onOpen()
.create();
}
What's the best way to share a readonly template to workers and it will directly grab latest employee list from master employee sheet without let them touching the code?
Thanks in million!
Have you tried:
function onOpen(e) {}
And with
ScriptApp.getProjectTriggers()
You probably can check if the trigger is already installed.
Alternate Solution:
Since most of your users are on the smart phones, you can do the following. You can publish your sheet, just select webpage option. And it will create link like so:
https://docs.google.com/spreadsheets/d/e/2PACX-1vSLilJvnFXiPW1ZsqRNQIVQDXasxYihzbEpxCO54nSpjv1IBIur1p5U7G8oJ2P8ThmZXYuV6LPJVZ8s/pubhtml?gid=725782869&single=true
Which you can directly share with your workers or you can use importHtml() function of the google sheets to import these publish values into your sheet like so
IMPORTHTML("https://docs.google.com/spreadsheets/d/e/2PACX-1vSLilJvnFXiPW1ZsqRNQIVQDXasxYihzbEpxCO54nSpjv1IBIur1p5U7G8oJ2P8ThmZXYuV6LPJVZ8s/pubhtml?gid=725782869&single=true","table",1)
Notes
1) The link can be viewed by anyone
2) The if viewed online it suppose to update every 5minutes or so.
3) If viewed in sheet using importHtml(), it will refresh when you reopen the sheet.
4) ImportHTML() does require to authorized like importRange() so will still work when copied.
5) You can publish your source sheet directly.

How do I execute a google script as 'owner' of a spreadsheet the user cannot view

Hopefully this is quite a simple question!
I've made a Google Script that writes to cells in a separate sheet "MasterSheet" (helped by several useful Q&As from here). This will ultimately be deployed embedded to multiple different sheets that I'm giving to individual users.
It works perfectly when the user has edit permissions on "MasterSheet", but I need that to remain private - i.e. not even viewable to anyone but me.
As background: In each 'user sheet', IMPORTRANGE is used to import the columns from 'MasterSheet' that that user is allowed to view, and then the script allows the user to add a comment to the MasterSheet.
I can view MasterSheet to see all the columns with comments from various users on one unified sheet, but the individual users shouldn't be able to view this.
The specific script for writing to the sheet is fairly generic:
function saveCommentToMasterSheet(form){
var company = form.company,
contact = form.contactselect,
comment = form.message ,
ss = SpreadsheetApp.openById("MASTERSHEET_ID").getSheetByName('MasterSheet');
if(company=="all"){
var row = findCell(contact);
} else {
var row = findCell2(contact,company);
}
//^The above finds the specific row number relating to the entry the user wants to comment on.
var cell2 = ss.getRange(row,11);
// ^In this case '11' is the column related to this sheet's specific user, I've made separate sheets for each user that are identical except this column number
cell2.setValue(comment);
}
I believe that I could make MasterSheet editable to anyone with the link, but I'd rather avoid that, particularly as the script is embedded in each spreadsheet so if the users just looked at it they'd find the MasterSheet id.
I understand that it's possible to run a script as me using the execute API, but if I'm honest, I struggled a little to figure out make that work.
Sorry if I'm asking a simple question - I've given it a good search and can't figure it out.
Many thanks!
Alex
N.B. This Running a google script from within a spreadsheet, but as a different user? looks like a similar question, but I'd really like to keep the comment system within the user's spreadsheet.
You can make a POST request to the master spreadsheet from the spreadsheets distributed to the users:
Apps Script Documentation - UrlFetchApp.fetch()
function saveCommentToMasterSheet(form) {//Function in the
//spreadsheets distributed to the users
var options,responseCode,url;
url = "https://script.google.com/macros/s/File_ID/exec";//Get from publishing
options = {};
options.method = 'post';
options.payload = form;
responseCode = UrlFetchApp.fetch(url, options).getResponseCode();
Logger.log('responseCode: ' + responseCode);//View the Logs
};
The above code will trigger the doPost(e) function in the master spreadsheet, and put the data into the event object e.
Then you can get the data out of e and write the data directly to what is the active spreadsheet, which is the master spreadsheet. Publish the Web App to run as "Me".
There are two versions of the published Web App; the "dev" version and the "exec" version. The "dev" version is always live with the latest changes, but should never be used in production. The "exec" version has a new version every time that you publish a the script again. To use the latest "exec" version in production, you must keep publishing the latest code.

protecting cells with google apps script (with dynamic button or command)

does anyone know if it is possible to make a google apps script for a google spreadsheet, that protects particular cells if a given situation occurs?
Fx. if an X occurs in "A1", then "A2" should be protected?
There's no API to manage cell protection yet. This feature request asks for this. You may want to star it to keep track of updates and kind of vote for it.
It is not. I don't believe that's the way that cell protection works. A cell is manually protected, or not protected at all.
There is no cell function or Script Object that enables that functionality.
The "feature request" referred by Henrique G. Abreu was fixed. Reference comment #165
Today, we have launched the ability to programmatically create and
manipulate protected ranges and protected sheets with Apps Script.
With the new Protection class in the Spreadsheet service, your scripts
can touch every aspect of range or sheet protection, just like in the
new UI. (The older PageProtection class, which had more limited
features, will be deprecated, but will stick around in case you need
to work with older spreadsheets. The new Protection class only applies
to the newer version of Sheets.)
You can find more details here:
http://googledevelopers.blogspot.com/2015/02/control-protected-ranges-and-sheets-in.html
https://developers.google.com/apps-script/releases/#february_2015
https://developers.google.com/apps-script/reference/spreadsheet/protection
i know that this is an old question and still - google "feture request" (posted by Henrique Abreu) has no progrees till now (end of 2014)
my hunch: this kind of feature would be a workaround for google forms (http://www.google.com/forms/about) when users are requested to edit cells that getting locked afterwards (by a criteria or another kind of logic)
i recently came accross a similar situation at my workplace, and my solution is very simple - make a form and share it with relevant users. the form fields will enable users to fill up the data they should be locked. that form will end up generating a sheet of information (updated online each time a form is filled). using "IMPORTRANGE" (https://support.google.com/docs/answer/3093340?hl=en) from another sheet will result in querying the "locked" information inside your relevant sheet. since the data is inside another sheet and is represented as a formula - it cannot be edited (at least not as a data)
i really considering a true workaround which can actually "lock" a cell (by replacing its value to the original by a script).. if there is such a request
I have a work-around, code follows
function protect_named_range(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var target_sheet = ss.getSheetByName('Sheet1');//change "sheet1" for the sheet your range is in
var array_range1 = target_sheet.getRange("A1:A1");
var array_range2 = target_sheet.getRange("A2:A2");
var range_test = array_range1.getValue();// or getFormula or what ever you want to test
if(range_test == "correct answer"){ //if cell A2 = "correct answer" then cell B2 becomes a named range called "named_range"
ss.setNamedRange("named_range", array_range2);
SpreadsheetApp.setActiveSheet(target_sheet);
var sheet = SpreadsheetApp.getActiveSheet();
var permissions = sheet.getSheetProtection();
permissions.setProtected(true);
target_sheet.setSheetProtection(permissions);
}
}
Sorry I have been away for awhile. This is the basic code I would use, however it doesnt actually protect the range "B2" if cell A2 = "correct answer. instead it protects the sheet named "sheet1" if cell A2 = "correct answer" so that only the spreadsheet owner may edit anything on the sheet.
I know this isnt exactly what your after, but I hope it can help.