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!!!!
Related
I have a very simple script that is triggered every 15 minutes to re-alphabetize a sheet based on that day's date. It works perfectly, except for one issue. The sheet is used by about 8-10 users at any given time. If a user is actively typing in a cell when the function is triggered and the sheet is resorted, then when they finish typing the cell they have edited the wrong cell. For example, if they start editing cell D24, and then after it is resorted that row becomes D28, then when they hit enter they will overwrite the new D24.
Is there any way I could incorporate something in the code to check if there are any cells that are actively being edited (greyed out because someone is inside it)? Or some other solution that would solve this issue?
function Alphabetize() {
var spreadsheet = SpreadsheetApp.getActive()
var now = new Date();
var name = Utilities.formatDate(now,"EST","MM-dd")
var day = now.getDay()
var sheet = spreadsheet.getSheetByName(name)
var col = sheet.getLastColumn()
var row = sheet.getLastRow()
var range = sheet.getRange(3, 1, row, col)
range.sort({column: 1, ascending: true})
}
Since modals don't work in this situation (time based triggers and other users) I figured out a slightly clunky work around. I added a row at the top of the sheet in the frozen section that is bright yellow and says "Press Enter. Sheet about to re-sort." multiple times so it's visible across the entire sheet. I then hid that row and inserted this code right before it sorts:
sheet.showRows(2)
Utilities.sleep(10000)
So it shows that row, waits for 10 seconds, sorts and then hides the row again with sheet.hideRows(2). Elegant, it is not. But it works on the time trigger and every user sees it. If there's a more elegant solution, I'm all ears.
Is there any way I could incorporate something in the code to check if there are any cells that are actively being edited (greyed out because someone is inside it)?
I think that this could be possible by using a web browser extension that reads the spreadsheet DOM but it will not very dependable as the web page source of many Google apps are generate automatically and anything could change without previous notice.
By one hand, the status identified by the web browser extension should be saved on a document / script store by using the Properties Service or the Cache Service so the time-driven trigger could read the status and by the other, the time-driven trigger save a flag to warn the user using the same services so the web browser could read them.
NOTE: In order to make this work, the web browser should call functions from the same project of the functions ran by the time-driver trigger.
By using Google Apps Script you could use spreadsheet.toast(...) to show a modeles and non so intrusive warning, then use a modal dialog to interrupt the user.
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.
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.
I'm working on a scoreboard automation process using Google Sheets, but I've come up with a little problem. I tried searching here but the question has been treated in a unclear way to me.
What am I trying to do is quite simple:
I want an automatic e-mail to be sent to a specific person IF cell value > X (threshold). I already know I need to use Google Apps Script for that, but I haven't found much interesting code lying around yet so I was wondering if you guys had an idea how that would work?
I tried something based on this thread with no success: How do I make a Google Sheet script send an email when a specific cell's value changes?
#Yvan1401,
You can use a script like this along with an installable trigger like onEdit or onChange to accomplish what you wish. To set up the installable trigger follow the steps below (found here):
From the script editor, choose Resources > Current project's
triggers.
Click the link that says: No triggers set up. Click here to add one
now.
Under Run, select the name of function you want to trigger. Under
Events, select From spreadsheet.
Select and configure the type of trigger you want to create (e.g. on
Change or on Edit trigger).
Optionally, click Notifications to configure how and when you will
be contacted by email if your triggered function fails.
Click Save.
Please see comments in code for portions to change.
function SendEmail() {
var ui = SpreadsheetApp.getUi();
var file = SpreadsheetApp.getActive();
var sheet = file.getSheetByName("Sheet1"); //Change as needed
if(sheet.getRange(5,1).getValue()>10){ //change row and column in get range to match what you need
MailApp.sendEmail("xxxxxcxxx#gmail.com", "subject", "message");
}
}
function sendMail() {
if (SpreadsheetApp.getActive().getSheetByName('Scoreboard').getRange('AL2:AL1000').getValue()<2.70) return;
MailApp.sendEmail("yvan#********.com", "******** new potential candidate available ! ", "Dear ****, a new candidate with strong potential is available for due dil !", {
name: "SOURCE NAME"
});
}
Here is an example of code that works the best and is the shortest. The thing is, I want it to scan only the cells that has changed (updated) within the column in question and not the old responses (data source is a form). What do you think i should do ? Because i don't want to erase the previous response within the same column.
edit: I use an "onChange" trigger.
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.