Prevent deletion of checkbox in sheet editable by many users - google-apps-script

I am using checkboxes in a sheet (and their functionality in themselves are exactly what I need).
The problems I am having is that the users who are able to edit the sheet, sometime (by mistake) delete the checkbox instead of simply checking/un-checking the checkbox.
Hence, I want the users to use the checkboxes, but not be able to delete them.
Is this possible somehow?
FYI: it is not possible to use Google Forms in the case.

Here you have an example on how to achive it using onOpen triggers
function onOpen(e)
{
var range = SpreadsheetApp.getActive().getSheets()[0].getRange(1,4,5);
var values = range.getValues();
for ( var val in values ) {
if( values[val] != true && values[val] != false ) {
values[val]= false;
}
}
range.insertCheckboxes(values);
}
You have to specify where are the checkboxes placed, in the code example provided there are 5 of them, starting from row 1 to row 5, and they are placed on the 4th column, so called D.
This is specified on the getRange(1,4,5) function:
getRange(InitialRow, Initial Column, Number of Rows)
for more documentation around the use of getRange() use the following reference.
This onOpen function will be executed every time that the spreadsheet is opened, and if on the specified fields there aren't false or true statements, it means that we lost the checkbox somehow, so it will introduce them again.

Related

Is there a way to have a checkbox toggle the text color of a set range?

I have a Google Sheets document and there is a dynamic list that gets created in a certain range (J19:L26) that has some personal data in it. Is there a way to make a checkbox or something quick I can click (even a button?) that can set the text to white or background to black to hide it to onlookers? I currently have the checkbox in cell M17
I have the following code that executes fine but then when I check the box in M17 nothing happens. Maybe I missed a step somewhere? I am new to Google sheets coding. I just wrote the function, tested it Runs, then closed it. Maybe I am just missing a step(s) in implementing the function to my sheet or my function Runs but doesn't do what I need it to?
function Privacy() {
var TheBoard = SpreadsheetApp.getActiveSpreadsheet();
var TheRange = TheBoard.getRange("Board!J19:L26");
if(TheBoard.getRange("Board!M17") == "TRUE")
{
TheBoard.getRange(TheRange).setFontColor('white');
}
}
There are a few things to correct. Here's a list:
When getting the status of the checkbox you're using TheBoard.getRange("Board!M17"). The getRange() method returns a reference to the range, not the value. After getting the range you can use getValue() on it to retrieve the values, so it should be TheBoard.getRange("Board!M17").getValue(), which will return true or false depending on the status of the checkbox.
You're comparing the value of the checkbox to == "TRUE". That makes it a string, and the checkbox value returns a boolean. In Javascript you're supposed to declare booleans as true or false, lowercase without quotes. That means the comparison should be == true instead.
The line TheBoard.getRange(TheRange).setFontColor('white'); returns an error. This is because getRange() expects a range in A1 notation or the coordinates to the row and column, but you're plugging TheRange into it, which is already a Range object. You already defined TheRange, so you don't need to "get" it again. The line should be just TheRange.setFontColor('white');
If you want to make the script run automatically when you click the checkbox you need to set it up as an onEdit() trigger. Then within the trigger check if the field that was edited was the checkbox before proceeding.
It's probably better to create a separate variable for the Sheet to avoid having to specify it in every range call.
May be a nitpick, but capitalizing variables makes the formatting color them like Classes, which may become confusing so I advise not doing that.
That said, here's the script with all the corrections:
function onEdit(e) {
if (e.range.getA1Notation() == "M17") {
var theBoard = SpreadsheetApp.getActiveSpreadsheet();
var theSheet = theBoard.getSheetByName("Board")
var theRange = theSheet.getRange("J19:L26");
if (theBoard.getRange("M17").getValue() == true) {
theRange.setFontColor('white');
} else {
theRange.setFontColor('black');
}
}
}
I recommend you familiarize yourself with the official documentation to better understand how each method works and apply them correctly.
Sources:
Sheet Class
Range Class
Triggers

How to automatically set a value in drop-down list based on a value from another drop-down list

I have a googlesheet with Columns A - P. Column B (GROUP) is a dropdown list and Column N (EXECUTION STATUS) is a drop-down list. I am trying to automatically set a particular value in a cell for GROUP based on the value that I selected in the EXECUTION STATUS dropdown list.
For Example:
GROUP has the following values in the drop-down list:
DEVELOPER
QA
LEVEL 1 SUPPORT
LEVEL 2 SUPPORT
EXECUTION STATUS has the following values in the drop-down list:
PASSED
FAILED
NOT EXECUTED
BLOCKED
Here is what I want to happen:
If I select FAILED as the EXECUTION STATUS, I want the GROUP to automatically change to DEVELOPER.
Here is my function:
function changeGroup(event)
{
var ColN = 14; // Column Number of "N"
var changedRange = event.source.getActiveRange();
if (changedRange.getColumn() == ColN)
{
// An edit has occurred in Column N
var state = changedRange.getValue();
var Group = event.source.getActiveSheet().getRange(changedRange.getRow(),ColN-12);
switch (state)
{
case "FAILED":
// Select DEVELOPER from dropdown list
Group.setValue("Developer");
break
}
}
}
I think my problem is the Group.setValue("Developer") line. SetValue is setting a text value. I am trying to set a value from the drop-down list. I'm not sure. Any suggestions?
The data validation rules distinguishes the uppercase and lowercase letters. I thought that this might be the reason of your issue. So if your values of data validation rules are DEVELOPER of the uppercase letter, please use it as follows.
Group.setValue("DEVELOPER");
Note:
In your script, I think that the OnEdit event trigger of the simple trigger can be used. But from your script, it seems that you are using the installable event trigger. So please confirm whether the function of changeGroup is installed as the installable OnEdit event trigger, again.
By the way, if you want to run the script for the specific sheet, please modify if (changedRange.getColumn() == ColN) as follows.
if (changedRange.getColumn() == ColN && changedRange.getSheet().getSheetName() == "Sheet1")
References:
Create an in-cell dropdown list
Class DataValidation
If I misunderstood your question and this was not the direction you want, I apologize.
Added:
In your case, the simple trigger can be also used. So how about testing the following script? When you use this script, please copy and paste the following script. Then, please change the dropdown list at the column "N".
function onEdit(event) // <--- Modified
{
var ColN = 14; // Column Number of "N"
var changedRange = event.source.getActiveRange();
if (changedRange.getColumn() == ColN)
{
// An edit has occurred in Column N
var state = changedRange.getValue();
var Group = event.source.getActiveSheet().getRange(changedRange.getRow(),ColN-12);
switch (state)
{
case "FAILED":
// Select DEVELOPER from dropdown list
Group.setValue("DEVELOPER");
break
}
}
}
Reference:
Simple Triggers

Google Sheets Script: IF Column A is filled, THEN write in Column E

I have a webform that's plugging entries into Google Sheets, so I can't place a formula within the Sheet's cells, otherwise the form will skip the row. My current script
function fillInTheBlank(e)
{
var row=e.range.getRow();
e.range.getSheet().getRange(row,5).setFormula('=IF(ISBLANK(A'+ row
+',1)),"","ready")');
}
IF column A is blank ignore, ELSE fill with "ready".
You are pretty close, and just need to check the value in code before setting it:
function onEdit(e) {
var col = e.range.getColumn();
// check Ax for any truthy value
if (e.range.offset(0, 1 - col).getValue()) {
e.range.offset(0, 5 - col, 1, 1).setValue("ready");
}
}
Note that the above requires no trigger, as it meets the qualification for the simple trigger, onEdit.
If your intent is to bind to the on form submit trigger, you'll need to name it something else (e.g. function addStatus(e)), and install the trigger for it. Note that in general, column A has the timestamp of the submission, and thus you do not need to check it for a value. Rather, if your goal is to simply add "ready" to the column next to the just-added form:
function addReady(e) {
var numQs = e.range.getNumColumns();
e.range.offset(0, numQs, 1, 1).setValue("ready");
}
Range offsets are always relative to the upper left cell, so offseting the number of questions and reducing the selection to a single row and column gives us the cell in the same row, adjacent to the questions.

Writing an 'Undo' Function for Google Spreadsheets Using GAS

Currently there is no undo() function for Google Apps Script in the Spreadsheet/Sheet/Range classes. There were a few issues opened on the Issue Tracker, I can only find one now (I don't know what Triaged means): here.
There have been suggested workarounds using the DriveApp and revision history but I took a look around and didn't find anything (maybe it's buried?). In any case, an undo() function is incredibly necessary for so many different operations. I could only think of one kind of workaround, but I haven't been able to get it to work (the way the data is stored, I don't know if it's even possible). Here is some pseudo -
function onOpen () {
// Get all values in the sheet(s)
// Stringify this/each (matrix) using JSON.stringify
// Store this/each stringified value as a Script or User property (character limits, ignore for now)
}
function onEdit () {
// Get value of edited cell
// Compare to some value (restriction, desired value, etc.)
// If value is not what you want/expected, then:
// -----> get the stringified value and parse it back into an object (matrix)
// -----> get the old data of the current cell location (column, row)
// -----> replace current cell value with the old data
// -----> notifications, coloring cell, etc, whatever else you want
// If the value IS what you expected, then:
// -----> update the 'undoData' by getting all values and re-stringifying them
// and storing them as a new Script/User property
}
Basically, when the Spreadsheet is opened store all values as a Script/User property, and only reference them when certain cell criteria(on) are met. When you want to undo, get the old data that was stored at the current cell location, and replace the current cell's value with the old data. If the value doesn't need to be undone, then update the stored data to reflect changes made to the Spreadsheet.
So far my code has been a bust, and I think it's because the nested array structure is lost when the object is stringified and stored (e.g., it doesn't parse correctly). If anyone has written this kind of function, please share. Otherwise, suggestions for how to write this will be helpful.
Edit: These documents are incredibly static. The number of rows/columns will not change, nor will the location of the data. Implementing a get-all-data/store-all-data-type function for temporary revision history will actually suit my needs, if it is possible.
I had a similar problem when I needed to protect the sheet yet allow edits via a sidebar. My solution was to have two sheets (one hidden). If you edit the first sheet, this triggers the onEdit procedure and reloads the values from the second sheet. If you unhide and edit the second sheet, it reloads from the first. Works perfectly, and quite entertaining to delete data on mass and watch it self repair!
As long as you will not add or remove rows and columns, you can rely on the row and column numbers as indices for historic values that you store in ScriptDb.
function onEdit(e) {
// Exit if outside validation range
// Column 3 (C) for this example
var row = e.range.getRow();
var col = e.range.getColumn();
if (col !== 3) return;
if (row <= 1) return; // skip headers
var db = ScriptDb.getMyDb();
// Query database for history on this cell
var dbResult = db.query({type:"undoHistory",
row:row,
col:col});
if (dbResult.getSize() > 0) {
// Found historic value
var historicObject = dbResult.next();
}
else {
// First change for this cell; seed historic value
historicObject = db.save({type:"undoHistory",
row:row,
col:col,
value:''});
}
// Validate the change.
if (valueValid(e.value,row,col)) {
// update script db with this value
historicObject.value = e.value;
db.save(historicObject);
}
else {
// undo the change.
e.range.getSheet()
.getRange(row,col)
.setValue(historicObject.value);
}
}
You need to provide a function that validates your data values. Again, in this example we only care about data in one column, so the validation is very simple. If you needed to perform different types of validation different columns, for instance, then you could switch on the col parameter.
/**
* Test validity of edited value. Return true if it
* checks out, false if it doesn't.
*/
function valueValid( value, row, col ) {
var valid = false;
// Simple validation rule: must be a number between 1 and 5.
if (value >= 1 && value <= 5)
valid = true;
return valid;
}
Collaboration
This undo function will work for spreadsheets that are edited collaboratively, although there is a race condition around storing of historic values in the script database. If multiple users made a first edit to a cell at the same time, the database could end up with multiple objects representing that cell. On subsequent changes, the use of query() and the choice to pick only the first result ensures that only one of those multiples would be selected.
If this became a problem, it could be resolved by enclosing the function within a Lock.
Revised the answer from the group to allow for range when user selects multiple cells:
I have used what I would call "Dual Sheets".
One sheet acts as a backup / master and the other as the active sheet
/**
* Test function for onEdit. Passes an event object to simulate an edit to
* a cell in a spreadsheet.
* Check for updates: https://stackoverflow.com/a/16089067/1677912
*/
function test_onEdit() {
onEdit({
user : Session.getActiveUser().getEmail(),
source : SpreadsheetApp.getActiveSpreadsheet(),
range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
authMode : "LIMITED"
});
}
function onEdit() {
// This script prevents cells from being updated. When a user edits a cell on the master sheet,
// it is checked against the same cell on a helper sheet. If the value on the helper sheet is
// empty, the new value is stored on both sheets.
// If the value on the helper sheet is not empty, it is copied to the cell on the master sheet,
// effectively undoing the change.
// The exception is that the first few rows and the first few columns can be left free to edit by
// changing the firstDataRow and firstDataColumn variables below to greater than 1.
// To create the helper sheet, go to the master sheet and click the arrow in the sheet's tab at
// the tab bar at the bottom of the browser window and choose Duplicate, then rename the new sheet
// to Helper.
// To change a value that was entered previously, empty the corresponding cell on the helper sheet,
// then edit the cell on the master sheet.
// You can hide the helper sheet by clicking the arrow in the sheet's tab at the tab bar at the
// bottom of the browser window and choosing Hide Sheet from the pop-up menu, and when necessary,
// unhide it by choosing View > Hidden sheets > Helper.
// See https://productforums.google.com/d/topic/docs/gnrD6_XtZT0/discussion
// modify these variables per your requirements
var masterSheetName = "Master" // sheet where the cells are protected from updates
var helperSheetName = "Helper" // sheet where the values are copied for later checking
var ss = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = ss.getActiveSheet();
if (masterSheet.getName() != masterSheetName) return;
var masterRange = masterSheet.getActiveRange();
var helperSheet = ss.getSheetByName(helperSheetName);
var helperRange = helperSheet.getRange(masterRange.getA1Notation());
var newValue = masterRange.getValues();
var oldValue = helperRange.getValues();
Logger.log("newValue " + newValue);
Logger.log("oldValue " + oldValue);
Logger.log(typeof(oldValue));
if (oldValue == "" || isEmptyArrays(oldValue)) {
helperRange.setValues(newValue);
} else {
Logger.log(oldValue);
masterRange.setValues(oldValue);
}
}
// In case the user pasted multiple cells this will be checked
function isEmptyArrays(oldValues) {
if(oldValues.constructor === Array && oldValues.length > 0) {
for(var i=0;i<oldValues.length;i++) {
if(oldValues[i].length > 0 && (oldValues[i][0] != "")) {
return false;
}
}
}
return true;
}

Google Apps Script - Spreadsheet run script onEdit?

I have a script that I use in my spreadsheet. It is at cell B2 and it takes an argument like so =myfunction(A2:A12). Internally the function gets info from a large range of cells.
Nothing seems to work. I tried adding it to
Scripts > Resources > Current Project Triggers > On edit
Scripts > Resources > Current Project Triggers > On open
How can I have this function update the result with every document edit?
When you are making calls to Google Apps services inside your custom function (like getRange and getValues etc), unfortunately there is no way of updating such custom functions with each edit, other than passing all of the cells that you are "watching" for editing.
And, perhaps even more frustratingly, the workaround of passing say a single cell that references all of your "watched" cells with a formula doesn't trigger an update - it seems that one needs to reference the "watched" cells directly.
You could pass GoogleClock() as an argument which will at least update the function output every minute.
But the advice from many members on this forum (who have much more knowledge about this stuff than me) would simply be: "don't use custom functions".
I am not sure if this exact code will work but you can try something like this...
function onEdit(event){
var activeSheet = event.source.getActiveSheet();
if(activeSheet.getName() == "ActiveSheetName") {
var targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TargetSheetName");
targetSheet.getRange(2,2).setValue("=myfunction(A2:A12)");
}
}
Assuming that B2 cell in on the sheet "TargetSheetName" and assuming that the edited cell is on the sheet "ActiveSheetName", the function onEdit will trigger when you edited any cell in any sheet. Since there is an if statement to check if that edited cell is on the sheet "ActiveSheetName" it will trigger only if the edited cell is on that sheet and it will set the B" cell to the value =myfunction(A2:A12), forcing it to update (i guess).
hope that i am correct and that i was helpful
I had a similar issue, for me I wanted to "watch" one particular cell to trigger my function.
I did the following (pretending A1 is the cell i am watching)
IF(LEN(A1) < 1, " ", customFunction() )
This successfully triggered if I ever edited that cell. However:
"Custom functions return values, but they cannot set values outside
the cells they are in. In most circumstances, a custom function in
cell A1 cannot modify cell A5. However, if a custom function returns a
double array, the results overflow the cell containing the function
and fill the cells below and to the right of the cell containing the
custom function. You can test this with a custom function containing
return [[1,2],[3,4]];."
from: https://developers.google.com/apps-script/execution_custom_functions
which makes it almost useless, but it might work for your case?
If the custom function is assigned to a project trigger it has more power so personally I ended adding it to "Scripts > Resources > Current Project Triggers > On edit"
and basically "watched a column" so it only did things if the current cell was within the "edit range". This is a bit of a bodge and requires some hidden cells, but it works for me at the moment
var rowIndex = thisspreadsheet.getActiveCell().getRowIndex();
var colIndex = thisspreadsheet.getActiveCell().getColumn();
//clamp to custom range
if(rowIndex < 3 && colIndex != 5)
{
return 0;
}
//check against copy
var name = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 5).getValue();
var copy = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).getValue();
if(name != copy )
{
///value has been changed, do stuff
SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).setValue(name);
}