How to freeze a range of cells when a checkbox is clicked? - google-apps-script

Screenshot for the sheet:
I just need someone to help me write simple code to freeze a range of cells when a certain checkbox is clicked.
I would like it so that when I click on the 'Complete' checkbox, all of the ones above it cannot be edited or changed anymore. Vise Versa when the 'Complete' checkbox is unchecked the ones above are editable. That simple.
The purpose of the sheet is to take attendance for a class. When I am done taking the attendance I don't want to be able to change it anymore (or risk clicking on the wrong checkbox). That's why the complete button is there.
Can anyone write the code for me, please?
(Freeze or seal or protect)
This code is not working (I am a beginner so sorry)
function onEdit() {
var sheet = SpreadsheetApp.getActive();;
var completedRow = sheet.getDataRange();
for (i = 2; i < 18; i++){
var isComplete = source.getRange(countRow, i).getValue();
if (isComplete === true){
source.getRange(2, i, countRow-1).protect();
}
}
}

Your code reflects the basic logic, though there are some syntax flaws. Hopefully this answer will help you understand and adapt that syntax.
The code doesn't doesn't take advantage of the Event Objects that are available to onEdit(e), which include the row, column and value of the edited cell. It's not compulsory to use the Event objects, but they certainly make life easier.
countRow isn't defined; and because you are working with a spreadsheet of finite length (20 rows); it is probably unnecessary. But it is a sensible idea to allow for bigger spreadsheets. Maybe something like var countRow = sheet.getLastRow(); would be a good alternative Doc Ref.
isComplete - we know that this is always on row 20; we also know that it will have a value of "true" or "false". So, you don't need a loop to define this row.
At some stage, you may want to "unprotect" a column; say at the start of a new term or year; so it's likely that checking row 20 for a value of "false" could be useful.
Your goal can probably be achieved in many ways. The following should be considered as just one option.
The main function is setup in an onEdit(e) simple trigger.
I also setup a custom menu (using onOpen) that gives you access to view all the protected columns, and to remove protection if you need to.
I've also left some Logger.log statements in the code that may enable you to check the value of certain fields at key stages of the code.
All-in-all, this code follows the same logic as your code, but with some more detail.
One last thing, this code is designed to work on a specific sheet by virtue of var sheet = ss.getSheetByName(sheetname); but you could just as easily change this to var sheet = SpreadsheetApp.getActiveSheet(); to make it work on multiple sheets in your spreadsheet.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
// set variable for last column
//Logger.log(JSON.stringify(e))
// set variables for edited cells,
var edittedRow = e.range.rowStart;
var edittedColumn = e.range.columnStart;
var newValue = e.value;
var headerrange = sheet.getRange(1, edittedColumn);
var headervalue = headerrange.getDisplayValue();
//Logger.log("DEBUG: The header range is "+headerrange.getA1Notation()+", and the value is "+headervalue);
// test if edit row =20, and the checkbox was ticked
if (edittedRow === 20 && newValue === "TRUE") {
//Logger.log("DEBUG: The 'ON' leg applies");
//Logger.log("DEBUG: edittedRow = "+edittedRow+", Editted column = "+edittedColumn+", and value = "+newValue);
// define the range to protect
var protectRangeOn = sheet.getRange(1, edittedColumn, 19, 1);
// protect the range - warning only.
protectRangeOn.protect().setDescription(headervalue)
.setWarningOnly(true);
//Logger.log("DEBUG1: protection set for "+protectRangeOn.getA1Notation());
}
//test if edit row=20, and the checkbox was unticked
if (edittedRow === 20 && newValue === "FALSE") {
//Logger.log("DEBUG: The 'OFF' leg applies");
//Logger.log("DEBUG: edittedRow = "+edittedRow+", Editted column = "+edittedColumn+", and value = "+newValue);
// define the range to unprotect
var protectRangeOff = sheet.getRange(1, edittedColumn, 19, 1);
var protections = sheet.getProtections(SpreadsheetApp
.ProtectionType.RANGE)
for (var i = 0; i < protections.length; i++) {
Logger.log("protections range name = " + protections[i]
.getDescription() + " - Header value = " + headervalue);
if (protections[i].getDescription() === headervalue) {
//Logger.log("DEBUG: OFF matches")
protections[i].remove();
}
}
//Logger.log("DEBUG2: protection unset for "+protectRangeOff.getA1Notation());
}
}
// Add a custom menu to the active spreadsheet to access Utilities
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Protection Utilities')
.addItem('Show all protections', 'uigetprotections')
.addItem('Remove all protections', 'removeallprotections')
.addToUi();
}
function removeallprotections() {
// remove all protections
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
var protections = ss.getProtections(SpreadsheetApp.ProtectionType
.RANGE);
Logger.log(protections);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
Logger.log(protection.getEditors())
if (protection.canEdit()) {
protection.remove();
}
}
// Display confirmation dialog
var ui = SpreadsheetApp.getUi();
var response = ui.alert('REMOVE ALL PROTECTION',
'Confirmed: Removed all protections', ui.ButtonSet.OK);
}
function uigetprotections() {
// generate a list of all RANGE protections
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
var protections = ss.getProtections(SpreadsheetApp.ProtectionType
.RANGE);
//Logger.log(protections);
var ui = SpreadsheetApp.getUi();
var protectioninfo = "";
if (protections.length != 0) {
for (var p = 0; p < protections.length; p++) {
//Logger.log("DEBUG: Date = "+protections[p].getDescription()+", Range = "+protections[p].getRange().getA1Notation());
protectioninfo = protectioninfo + "Date: " + protections[p]
.getDescription() + ", Range = " + protections[p].getRange()
.getA1Notation() + "\n";
}
var response = ui.alert('SHOW ALL PROTECTIONS', protectioninfo, ui
.ButtonSet.OK);
} else {
var response = ui.alert('SHOW ALL PROTECTIONS',
"There were no protected ranges", ui.ButtonSet.OK);
}
}

Related

How to refresh a filter onEdit() in Google sheets?

I have a sheet where I fill data with Hlookups depending on the values I choose in dropdowns.
I want to filter (hide) the rows that have a NULL or blank value in column 3 each time I change the values in the dropdowns (which changes the whole dataset).
If I create a normal filter, it doesn't refresh when the data changes.
var PARAMETER_ROW_NUMBER = 5; //The parameters goes from Row 1 to this Row
var PARAMETER_COLUMN_NUMBER = 2; //The column where the dropdowns with the parameters for the VLOOKUPs are
function onEdit()
{
var thisSheet = SpreadsheetApp.getActiveSheet();
if( thisSheet.getName() == "By Place" )
{
var cell = thisSheet.getActiveCell();
var cellRow = cell.getRow();
var cellColumn = cell.getColumn();
if( cellColumn == PARAMETER_COLUMN_NUMBER && cellRow <= PARAMETER_ROW_NUMBER)
{
setFilter(); // Execute the filter to clean null rows each time I change the values in the dropdowns
var rowDiff = PARAMETER_ROW_NUMBER - cellRow;
cell.offset( 1, 0, rowDiff).setValue(''); // As the parameters are dependent dropdowns, I clear the dropdowns if one changes
}
}
}
function setFilter()
{
var ss = SpreadsheetApp.getActiveSheet();
var rang = ss.getDataRange();
var filtercriteria = SpreadsheetApp.newFilterCriteria().setHiddenValues([' ','']).build();
var filter = rang.getFilter() || rang.createFilter();
filter.setColumnFilterCriteria(3, filtercriteria); // I want to hide the rows which has a null or blank in column 3
}
The setFilter() function doesn't work.
The array you're using to set the hidden values is not correct, try your code like this:
function setFilter()
{
var ss = SpreadsheetApp.getActiveSheet();
var rang = ss.getDataRange();
var filterCriteria = SpreadsheetApp
.newFilterCriteria()
.setHiddenValues(['NULL', ''])
.build();
var filter = rang.getFilter() || rang.createFilter();
filter.setColumnFilterCriteria(3, filterCriteria);
}
Also, if you want to see the logs from your onEdit executions, you can check them on your Apps Script file by clicking in View - > Executions, there you will be able to see the errors you are getting.
Docs
I used these docs to help you:
setColumnFilterCriteria(columnPosition, filterCriteria).
Class FilterCriteriaBuilder.

protect/unprotect range if a specific cell has changed to a specific value

i want to make a specific range not changeable. but if a specific cell has changed from "no" to "yes" then the range should be changeable. and if the specific cell changed from "yes" to "no" then the range should be not changeable again.
How could I realize that in googe app script?
Thanks for help.
Something like below should get you started. Basically, protecting stuff in Apps Script is easy; Removing the protection is more tedious. Because of that, I'd recommend adding a description to the protected range in question (as I've done with "Test1"), after which, the below loop would find and remove it with ease.
I've attached it to this demo spreadsheet so you have context with the data I was working with.
function onEdit(event){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheets()[0]; // 0 == first sheet.
var ee = event.source.getActiveRange().getA1Notation();
if (ee == "A2") { // Check if edited cell is the one we're watching.
if (event.value == "yes"){ // If the value == "yes", do stuff.
var protections = sh.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
if (protections[i].getDescription() == 'Test1') { // If protection description == "Test1"...
protections[i].remove(); // then remove protection.
}
}
}
}
}
function setProtected(){
// Sets the desired range to protected with "Test1" description for demonstration.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheets()[0];
var protectedRows = sh.getRange("C2:C11");
protectedRows.protect().setDescription("Test1");
}

Script for Google sheet to change a cells value based on another cells value (in a column range)

Here is the scenario:
Column E in my Googlesheet has a dropdown list of Yes and No. Everytime the user answers No, I want the corresponding cell in Column G to have the words "Not Applicable". But if user answers Yes, I want that cell in G to have another dropdown list of Yes and No.
I tried to build on the script that I got from this thread:
Google Sheets formula to change a cells value based on another cells value
It's almost perfect, but I can't make it to work for a range (an entire column, preferrably). Any advice would be appreciated :)
Since I wasn't getting any success trying to tweak it for my own use, here's a copy code from the mentioned thread:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getActiveSheet()
var nota=e.range.getA1Notation()
if(nota=="E10"){
var val=e.range.getValue()
if(val=="Levy"){
s.getRange("E11").setDataValidation(null)
s.getRange("E11").setValue("Monthly")
}
else{
s.getRange('E11').clearContent()
var cell = s.getRange('E11');
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Triannual', 'Quarterly']).build();
cell.setDataValidation(rule);
}}}
And here's my dumb attempt:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getActiveSheet()
var nota=e.range.getA1Notation()
if(nota=="E2:E500"){
var val=e.range.getValue()
if(val=="No"){
s.getRange("G2:G500").setDataValidation(null)
s.getRange("G2:G500").setValue("Not Applicable")
}
else{
s.getRange('G2:G500').clearContent()
var cell = s.getRange('G2:G500');
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Yes','No']).build();
cell.setDataValidation(rule);
}}}
You need to loop a range of cells, which is why the original script isn't working. .getValue() returns a single cell, you can't have a range (as in your edited script).
This script will look at the entire page and loop it each time. This is preferable because you don't have to keep data in order. In other words, you can jump around Column E and mark things "Yes" or "No" as they come up. Blank cells are ignored.
function addValidation() {
// Get the spreadsheet and active sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
// Get the data as an Array you can iterate and manipulate
var data = sheet.getDataRange().getValues();
// Store a rule to use for the Data Validation to be added if ColE == "Yes"
var rule = SpreadsheetApp.newDataValidation().requireValueInList(["Yes", "No"]).build();
// Loop the sheet
for(var i=0; i<data.length; i++) {
// Test ColE. Note that the array is 0-indexed, so A=0, B=2, etc...
// To change which columns you're testing, change the second value.
if(data[i][4] == "Yes") {
// If it's "Yes," add the Data Validation rule to Col G for that row.
// Note that .getRange() is _not_ 0 indexed, which is why you need `i+1` to get the correct row
sheet.getRange(i+1, 7).clear().setDataValidation(rule);
// If ColE == "No," mark ColG as "Not Applicable"
} else if(data[i][4] == "No") {
sheet.getRange(i+1, 7).clearDataValidations().setValue("Not Applicable");
}
}
}
Also note that this will change values as you change Col E. So, if you change a "Yes" to a "No," Col G will be changed to "Not Applicable."
for (var i = 0; i < 5000; i++) {
function checkData() {
if(SpreadsheetApp.getActiveSheet().getRange("E" + i).getValue() == "No"){
SpreadsheetApp.getActiveSheet().getRange("G" + i).setValue('Not Applicable');
}
else{
SpreadsheetApp.getActiveSheet().getRange("G" + i).setValue('Applicable');
}
}
}
Hope this works for you:)
Btw getting the range of 5000 rows will be VERY slow! Here is another way you can do it faster!
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tsSheet = ss.getSheetByName("YOUR SHEET NAME AT THE BOTTOM OF THE PAGE");
var tsRows = parseInt(tsSheet.getLastRow());
for (var i = 0; i < tsRows; i++) {
function checkData() {
if(SpreadsheetApp.getActiveSheet().getRange("E" + i).getValue() == "No"){
SpreadsheetApp.getActiveSheet().getRange("G" + i).setValue('Not Applicable');
}
else{
SpreadsheetApp.getActiveSheet().getRange("G" + i).setValue('Applicable');
}
}
}
EDIT:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tsSheet = ss.getSheetByName("YOUR SHEET NAME AT THE BOTTOM OF THE PAGE");
var tsRows = parseInt(tsSheet.getLastRow());
for (var i = 0; i < tsRows + 1; i++) {
function checkData() {
if(SpreadsheetApp.getActiveSheet().getRange("E" + i).getValue() == "No"){
SpreadsheetApp.getActiveSheet().getRange("G" + i).setValue('Not Applicable');
}
else{
SpreadsheetApp.getActiveSheet().getRange("G" + i).setValue('Applicable');
}
}
}

Google Apps Script - Make onEdit() recognize setValue() changes

I have a Spreadsheet with some functions. One of them is a onEdit(event) function that copies some values to other sheets based on conditions. This is the code (simplified but with the important parts intact):
function onEdit(event) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = event.source.getActiveSheet();
var r = event.range;
if(s.getName() === "Lista" && r.getColumn() === 9 && r.getValue() === "Posicionada") {
var sheetname = s.getRange(r.getRow(),3).getValue();
var columnRef = s.getRange(r.getRow(),4).getValue();
var row = s.getRange(r.getRow(),5).getValue();
var targetSheet = ss.getSheetByName("Mapa " + sheetname);
var headers = targetSheet.getRange(1, 1, 1, targetSheet.getLastColumn());
for (var i = 0; i < headers; i++) {
if (headers[i] === columnRef) {
break;
}
}
var column;
if (columnRef === "A1") {
column = 2;
}
else if (columnRef === "A2") {
column = 3;
}
else if (columnRef === "B1") {
column = 4;
}
else if (columnRef === "B2") {
column = 5;
}
if (sheetname === "N2") {
row = row - 30;
}
if (sheetname === "N3") {
column = column - 10;
row = row - 42;
}
targetSheet.getRange(row,column).setValue(s.getRange(r.getRow(), 1, 1, 1).getValue());
}
}
The code works as it should when I manually edit the cell. But, I have a code that edit the cell when the user press a button in a sidebar, this is the code:
function positionMU(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cell = ss.getActiveCell().activate();
var cellLevel = cell.offset(0,2);
var cellLetter = cell.offset(0,3);
var cellNumber = cell.offset(0,4);
var cellStatus = cell.offset(0,8);
var dbq = "Posicionada";
var fora = "Pendente de recebimento";
if (cellStatus.getValue() == "Aguardando posicionamento"){
cellStatus.setValue(dbq); //attention in this line
}
else if (cellStatus.getValue() == "Aguardando saĆ­da"){
cellStatus.setValue(fora);
var cellExitDate = cell.offset(0,6);
cellExitDate.setValue(getDate());
}
}
As you can see, this function change the cell content with setValue(), but, when I use this function, the value of the cell changes, but the onEdit() trigger doesn't work.
How can I make the onEdit() trigger recognize changes made with setValue()?
You are right. onEdit() only triggers if the range is edited manually. As can be seen here, onEdit() triggers when a value is changed by the user.
I tested the function by making function to insert values into a column for which my onEdit responds and nothing happens. Including various other techniques that I could think of. Best thing to do here is to suggest this as an enhancement on App Script's Issue Tracker.
However, I made it work by writing another function to be called when another function in the script makes changes to the sheet. These are the test functions I wrote:
function addValues()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
var range = sheet.getDataRange();
var book = "Book";
var cancel = "Cancel";
var maxRow = range.getLastRow()+1;
for(var i=0; i<4; i++)
{
if (i%2 == 0)
{
sheet.getRange(maxRow, 1).setValue(book);
autoChanges(maxRow);
}else{
sheet.getRange(maxRow, 1).setValue(cancel);
autoChanges(maxRow);
}
maxRow++;
}
}
autoChanges function:
function autoChanges(row)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
var range = sheet.getDataRange();
var data = range.getValues();
var response = "";
sheet.getRange(row, 2).protect();
response = data[row-1][0];
if (response == "Book")
{
sheet.getRange(row, 2).canEdit();
}else{
sheet.getRange(row, 2).setValue("--NA--");
}
}
Not the most elegant solution but this seems to be the only workaround for what you are trying to do.
There are some very good reasons why calling range.setValue() doesn't trigger the onEdit event, and most of them have to do with infinite recursion. In fact, you call setValue() yourself WITHIN onEdit(). This would trigger a recursive call, and from what I can see, you have no provision for handling the base case and thus your code would explode if setValue() did what you want.
Why not simply take all of your code out of your event handler, and put it into another function:
function onEdit (e) {
return handleEdits(e.range);
}
function handleEdits(r) {
s = r.getSheet();
ss = s.getParent();
//the rest of your code should drop right in.
}
then, inside your autoChanges function, go ahead and call handleEdits, passing it an appropriate range, after your call to setValue().
If you like to play with fire, and I personally do, you can call the onEdit(e) function after you make a change. Just send in an object whatever is called and used by the e object.
For me, I just needed to add:
var e={};
e.range=the range you are making a change to in the script;
e.source = SpreadsheetApp.getActiveSpreadsheet();//or make sure you have the sheet for wherever you are setting the value
e.value=whatever value you are setting
e.oldValue=if you onEdit needs this, set it here
//if you are using any of the other standard or special properties called by your onEdit...just add them before calling the function.
onEdit(e);//call the function and give it what it needs.

How to add a button per row in google spreadsheet?

I have a simple spreadsheet with multiple rows I want to add button to each of it automatically.
On click this button will remove the entire row and move it to another spreadsheet.
I've looked online and couldn't find a way to add buttons to spreadsheets. Some people suggest to insert drawings and assign macros, but looks like the latest version lacks this feature.
I've also read about google app sript. I was able to add buttons to a menu, but not directly to each row of a spreadsheet.
Do you have any advice or suggestion on how to better achieve this?
I suggest the you create a column(say 10) at the end of each row with a List Data Validation of "Move Row" and create an onEdit function like so
function onEdit(eventInfo) {
if (eventInfo.range.getColumn() == 10 && eventInfo.value == "Move Row")
{
var ss = eventInfo.source;
var row = eventInfo.range.getRow();
var range = ss.getRange(row, 1, 10) // this is your range, do with it what you will
}
}
So this script is not entirely my own work, but the results of something I was working on with help.
It will look in a source sheet, look at a column you specify (by number), and look for a value in this sheet.
If matched, it will move that row to another sheet, within the spreadshet, of your choosing.
function moveRowInternalSheet()
{
var sourceSheet = "";
var columnNumberToWatch = ;
var valueToFind = "";
var destinationSheet = "";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(sourceSheet);
var values = sheet.getDataRange().getValues();
var counter = 1; //starts looking in second row, due to header
var archive = [];
while (counter < values.length)
{
if (values[counter][columnNumberToWatch - 1] == valueToFind)
{
archive.push(values.splice(counter, 1)[0]);
}
else
{
// If the value to find is not found. Then increment the counter by 1
counter++;
}
}
var archiveLength = archive.length;
if (!archiveLength) return;
var targetSheet = ss.getSheetByName(destinationSheet);
var lastRow = targetSheet.getLastRow();
var requiredRows = lastRow + archiveLength - targetSheet.getMaxRows();
if (requiredRows > 0) targetSheet.insertRowsAfter(lastRow, requiredRows);
targetSheet.getRange(lastRow + 1, 1, archiveLength, archive[0].length).setValues(archive);
sheet.clearContents();
sheet.getRange(1, 1, values.length, values[0].length).setValues(values);
}