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

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.

Related

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.

Google Script Automatic Range Protection

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!!!!

Running Simple "Clear" Script Without Signing In (Google Sheets)

New to java, as well as scripting in google.
I am a graduate student, running a tabletop (emergency management exercise) event in a few weeks.
I have created a spreadsheet in which I have multiple participants rating their organizational performance. Part of that ranking involves a process where the participants will need to enter numbers into 4 cells which then spits out a result. I need those 4 cells to clear upon the press of a button, so i have poked around and found a script that does what i need it to do:
function clearRangeG1() {
//replace 'Sheet1' with your actual sheet name
var sheet = SpreadsheetApp.getActive().getSheetByName('G1 2x2');
sheet.getRange('C18:E18').clearContent();
sheet.getRange('M18:P18').clearContent();
sheet.getRange('C28:E28').clearContent();
sheet.getRange('M28:P28').clearContent();
}
function clearRangeG2() {
//replace 'Sheet1' with your actual sheet name
var sheet = SpreadsheetApp.getActive().getSheetByName('G2 2x2');
sheet.getRange('C18:E18').clearContent();
sheet.getRange('M18:P18').clearContent();
sheet.getRange('C28:E28').clearContent();
sheet.getRange('M28:P28').clearContent();
}
function clearRangeG3() {
//replace 'Sheet1' with your actual sheet name
var sheet = SpreadsheetApp.getActive().getSheetByName('G3 2x2');
sheet.getRange('C18:E18').clearContent();
sheet.getRange('M18:P18').clearContent();
sheet.getRange('C28:E28').clearContent();
sheet.getRange('M28:P28').clearContent();
}
function clearRangeG4() {
//replace 'Sheet1' with your actual sheet name
var sheet = SpreadsheetApp.getActive().getSheetByName('G4 2x2');
sheet.getRange('C18:E18').clearContent();
sheet.getRange('M18:P18').clearContent();
sheet.getRange('C28:E28').clearContent();
sheet.getRange('M28:P28').clearContent();
}
This works PERFECTLY, none of the code needs to be changed; however, if I am not signed into a google account, I cannot run or use the CLEAR button I have created.
I have looked into this issue, and I have come across information that has lead me to believe there is a way for me to create an API or add on that can run a token so that the button always functions believing my account is activating the submission.
Is this information accurate? Is there actually a way to run a script without needing to sign in? The only thing i need is for this clear button to work for my workshop participants, who may not all have google accounts. If this is indeed possible, could someone direct me towards a proper resource for doing so? I have investigated "OAuth2 Authorization" but the subject seems like it could take me a day or two to sort through. I am really crunched for time, and I do not wish to investigate this further if the process is simple something google does not allow.

Google script: unprotect a sheet through function when user has no right to do so

I have a spreadsheet with different sheets in Google sheet, 3 users can edit each one a sheet (protections are set, each user can edit only one sheet). They all can execute a google script function that writes what they edited in a summary sheet. I don't want anyone to be abble to edit the summary sheet, so I set myself as the only available editor.
So my problem is to authorize the 3 users, only through the google script function, to write in the summary sheet. I tried to use the following function :
var unprotected = summarySheet.getRange('G3:G10');
protection.setUnprotectedRanges([unprotected]);
but since the users are not allowed to edit the summary sheet, and since the function is run with the active user, so they can't give themselves the right to unprotect a range in the summary sheet... Do you know how to workaround this problem?
Thanks a lot!
I see two script-based choices, one easy and one quite hard, and one sheet-based choice, that is easiest:
Easy:
You run the "summarize" script instead of them or, you set the summarize script run on a trigger out of your account. Then you actually leave protections alone. You could set the summarize script to run on open with error catching if the user doesn't have the necessary authority to unprotect the summary sheet and/or write to the summary sheet.
Hard:
When they run the "summarize" script it calls a published standalone script that has been given the authorization to make the necessary protection changes. I'll be honest, I wouldn't be able to code this but have seen/heard of similar implementations.
Easiest:
Finally, I want to make sure you've considered having the summary sheet itself contain the necessary formulas, parsing, etc. to summarize data from the other sheets without any need of scripts for this aspect of the sheet. The sheet could call custom functions as needed if the parsing or other summarization functionality is beyond built-in functions' capabilities. The sheet could stay fully protected and update itself in real time as users enter data (no need for users to trigger the summary creation, unless spreadsheet settings have auto-recalculate turned off).
Edited to add: put in A1 of Summary sheet something like:
=summarize()
And have that custom function return a 2-dimensional array of the summarized data.

Google Apps Script - Editors Execute Script on Spreadsheet

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.