preventing changes to a cell in google spreadsheet - google-apps-script

I want to prevent changes to column K in google spreadsheet. Whatever value is there, I do not want it changed. I do not like the protection feature as it makes what I consider an ugly display.
My code. Unfortunately, it does absolutely nothing. The intent was to take whatever the current value is in the cell, save it, and then write it back on exit of the cell instead of saving whatever changes might have been made to the cell. The cell will either be blank to start, or will already have been modified to contain a date & time. Whatever the current contents blank or not, it should retain the same value after leaving the cell.
function onEdit() {
var s = SpreadsheetApp.getActiveSheet();
var r = s.getActiveCell();
var columnNum=r.getColumn()
// if column is K then prevent any changes
if (columnNum == 11) {
var dateCell = s.getRange(r.getRow(), 11);
var v=dateCell.getValue();
dateCell.setValue(v);
}
};

This might help you. A workaround to your problem I came up with till Google have a proper script API to protect ranges. This one is using the validation function and it works.
function setRangeProtection(rangeToProtect) {
var firstRow = rangeToProtect.getRow();
var noOfRows = rangeToProtect.getHeight();
var firstColumn = rangeToProtect.getColumn();
var noOfColumns = rangeToProtect.getWidth();
var rangeToProtectValues = rangeToProtect.getValues();
var rangeToProtectFormulas = rangeToProtect.getFormulas();
var cellRow = firstRow;
for (var i=0 ; i<noOfRows ; ++i) {
var cellColumn = firstColumn;
for (var j=0 ; j<noOfColumns ; ++j) {
var cell = sheet.getRange(cellRow, cellColumn);
if (rangeToProtectFormulas[i][j] == "") {
var rangeToProtectContent = rangeToProtectValues[i][j];
} else {
var rangeToProtectContent = rangeToProtectFormulas[i][j];
}
var rules = SpreadsheetApp.newDataValidation()
.requireTextEqualTo(rangeToProtectContent)
.setAllowInvalid(false)
.setHelpText('Don\'t mess with the DJ!')
.build();
cell.setDataValidation(rules);
++cellColumn;
}
++cellRow;
}
}
The only small issue with this is that with cells that contain formulas a small red triangle appears and a message is displayed when you hover over it. Couldn't get rid of that one. If you find a solution for that let me know. Removing the help text doesn't help as it returns to a default mode.

Related

How to clear conditional formatting (not all formatting) in Google Apps script

I need a way to remove all conditional formatting by running a script (my client will be using this and he doesn't want to have to repeat the process of removing conditional formatting for each worksheet in each of a large number of spreadsheet files).
Is there any way to do this via Google Apps script? All I see is .clearFormat(), which unfortunately clears all formatting, of which a lot should not be deleted (eg, font color, bg color, font, font weight, font rotation, cell outlines)
How to do this in such a way that only one button needs to be pressed for each spreadsheet file?
Google Apps Scripts now supports removing conditional formatting using clearConditionalFormatRules
var sheet = SpreadsheetApp.getActiveSheet();
sheet.clearConditionalFormatRules();
https://developers.google.com/apps-script/reference/spreadsheet/sheet#clearconditionalformatrules
This is possible with Google Sheets API v4, which Apps Script can access via Advanced Sheets Service (note that it must be enabled before use, as the linked page instructs). Here is a script that deletes all conditional formatting rules in Sheet1 of the current spreadsheet (you'll want to loop over sheets, etc).
function clearSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
var sheet = ss.getSheetByName("Sheet1");
var sheetId = sheet.getSheetId();
var format_req = {
"requests": [{
"deleteConditionalFormatRule": {
"index": 0,
"sheetId": sheetId
}
}]
};
var string_req = JSON.stringify(format_req);
while (true) {
try {
Sheets.Spreadsheets.batchUpdate(string_req, ssId);
}
catch(e) {
break;
}
}
}
Each conditional format rule has a 0-based "index". Deleting the rule with index 0 causes other indices to decrease by 1. So the loop continues deleting index = 0 rule until there isn't one, and an error is thrown (and caught, exiting the loop).
This is a weird way of sending requests: I'd much rather send one batch request like this:
var format_req = {
"requests": [
{
"deleteConditionalFormatRule": {
"index": 0,
"sheetId": sheetId
}
},
{
"deleteConditionalFormatRule": {
"index": 1,
"sheetId": sheetId
}
}]
};
but to do this, one must know how many conditional formatting rules are there (and I don't see how one would find out). If you ask for more rules to be deleted than exist in the sheet, the entire request fails and nothing is deleted.
Without advanced service
With plain Apps Script, the best one can do is this:
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns());
var backgrounds = range.getBackgrounds();
var fontColors = range.getFontColor();
var fontFamilies = range.getFontFamilies();
// ... other get methods from https://developers.google.com/apps-script/reference/spreadsheet/range
// tricky part: modify the backgrounds, replacing the colors used in conditional formatting by white
range.clearFormat();
range.setBackgrounds(backgrounds)
.setFontColors(fontColors)
.setFontFamilies(fontFamilies)
// .set other things
Here I am assuming that conditional formatting affects only cell backgrounds. If one is unable to filter the background colors (which requires knowing exactly what colors were used in conditional formatting rules), the effects of conditional formatting will become ordinary background color, which is very undesirable... it may be better to forego setting the background colors at all.
So after my comment to if....
but to do this, one must know how many conditional formatting rules are there (and I don't see how one would find out)
decided to extend his code:
function get_clear_Formatting() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
// 1 part - get conditional formatting data for all sheets in active spreadsheet
var params = {
'fields': "sheets(properties(title,sheetId),conditionalFormats)"
};
var getFormatResult = Sheets.Spreadsheets.get(
ssId,
params
);
var sheets = getFormatResult.sheets;
var ConditionalFormatIndex = {
"sheetID" : [],
"sheetTitle" : [],
"formatRulesCount" : []
}
for (var i = 0; i < sheets.length; i++) {
ConditionalFormatIndex.sheetID[i] = sheets[i].properties.sheetId;
ConditionalFormatIndex.sheetTitle[i] = sheets[i].properties.title
ConditionalFormatIndex.formatRulesCount[i] = (sheets[i].conditionalFormats) ?
sheets[i].conditionalFormats.length :
0;
}
// 2 part - clear all conditional formatting in an all sheets in active spreadsheet
var ClearFormat_req = []
for (var i = 0; i < ConditionalFormatIndex.sheetID.length; i++) {
if ( ConditionalFormatIndex.formatRulesCount[i] ) {
for (var cf = 0; cf < ConditionalFormatIndex.formatRulesCount[i]; cf++) {
ClearFormat_req.push(
{
"deleteConditionalFormatRule": {
"index": 0,
"sheetId": ConditionalFormatIndex.sheetID[i]
}
});
}
};
}
Sheets.Spreadsheets.batchUpdate({'requests': ClearFormat_req}, ssId);
}
Copy a cell that doesn't have any conditional formatting.
Select the cell(s) you want to remove the conditional formatting from.
Edit -> Paste special -> Paste conditional formatting only.
A bit late, but I found a way, if that may help someone.
remove every conditional formatting on ranges intersecting the one in parameters
Then leave every other conditional formatting untouched (actually : rebuild it).
function test(){
var sh=shWork;//define your sheet
var r= sh.getRange("A3:A6");//example
clearEveryConditionalFormattingOnRange(sh,r)
}
function clearEveryConditionalFormattingOnRange(sh,r){
//build a parallel rules at looping on initial rule, in order to rebuild it without unwanted elements
//get rules
var rules=sh.getConditionalFormatRules();
//create new rules
var a_newRules= new Array();
//loop on rules
for (var i=0;i<rules.length;i++){
//create new currentRanges
var a_newCurrentRanges=new Array();
//loop on ranges from rule
var currentRule=rules[i];
var currentRanges=currentRule.getRanges();
for (var j=0;j<currentRanges.length;j++){
var currentRange=currentRanges[j];
var testIfIntersect_OK=RangeIntersect(r,currentRange);
//add this range to a_newCurrentRanges
if (!testIfIntersect_OK){
a_newCurrentRanges.push(currentRange);
}//if (testIfIntersect_OK){
}//for (var j=0;j<currentRanges.length;j++){
//create then add new rule to a_newRules
if (a_newCurrentRanges.length>0){
var a_newRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied(currentRule.getBooleanCondition().getCriteriaValues())
.setBackground(currentRule.getBooleanCondition().getBackground())
.setRanges(a_newCurrentRanges)
.build();
a_newRules.push(a_newRule);
}//if (a_newCurrentRanges.length>0){
}//for (var i=0;i<rules.lengthi++){
sh.setConditionalFormatRules(a_newRules);
}
//returns true if intersection between range1 and range2
function RangeIntersect(R1, R2) {
var LR1 = R1.getLastRow();
var Ro2 = R2.getRow();
if (LR1 < Ro2) return false;
var LR2 = R2.getLastRow();
var Ro1 = R1.getRow();
if (LR2 < Ro1) return false;
var LC1 = R1.getLastColumn();
var C2 = R2.getColumn();
if (LC1 < C2) return false;
var LC2 = R2.getLastColumn();
var C1 = R1.getColumn();
if (LC2 < C1) return false;
return true;
}
I ended up finding a solution based on David Friedman's answer. This script successfully removed the conditional format from just one column (D), and left the conditional formats in other columns unchanged.
// clearConditionalFormat
// Data must have header row that does NOT have conditional formatting
// Otherwise you must identify some other cell on the sheet that does not
// have conditional formatting
function test(){
var sheetName = "Sheet13"; // replace with your sheet's name
var rangeText = "D3:D"; // replace with the your range
clearConditionalFormat(rangeText,sheetName);
};
function clearConditionalFormat(rangeText,sheetName){
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var rangeText = rangeText.toString().toUpperCase();
var range = ss.getRange(rangeText).activate();
var rangeTextSplit = rangeText.split(":");
// example: returns AA22 from AA22:AZ37
var rangeFirstLetter = rangeTextSplit[0].replace(/[0-9]+/g, "");
// example: returns AA from AA22
var rangeRowNum1 = rangeTextSplit[0].replace(/[A-Z]+/g, "")*1;
// example: returns the 22 of AA22:AZ37
var rangeHeaderText = rangeFirstLetter + (rangeRowNum1 - 1);
sheet.getRange(rangeHeaderText)
.copyTo(range,SpreadsheetApp.CopyPasteType
.PASTE_CONDITIONAL_FORMATTING, false);
};

Google sheets, make active range the top left cell of the window

Essentially, I have a few sheets with the following setup:
A few frozen title rows at the top of the sheet, underneath that many rows of data all with a date in column A. The following function allows me to jump to 'today' on any sheet (well, technically the closest day in the future if there isn't data for today).
function goToToday() {
var workbook = SpreadsheetApp.getActiveSpreadsheet()
var currentSheet = workbook.getActiveSheet().getName();
if (currentSheet !== "Calculations") {
workbook.getSheetByName(currentSheet).setActiveRange(workbook.getSheetByName(currentSheet).getRange(1,1))
var today = new Date()
var rowNumber = 4
var numOfRows = workbook.getSheetByName(currentSheet).getLastRow() - 3
var dates = workbook.getSheetByName(currentSheet).getRange(4,1,numOfRows,1).getValues()
for(i = 0; i < numOfRows; i++) {
if(dates[i][0] > today) {
break
}
else {
rowNumber++
}
}
workbook.getSheetByName(currentSheet).getRange(rowNumber, 1).activate()
}
}
What's bugging me is if active cell prior to running the function is below the returned 'today' cell of the function, the cell is returned as the top left cell in the window, which is perfect. If however the current active cell is above the returned cell, the cell is returned near the bottom of the window. How can I make the function return the cell consistently as the top left cell of the window? I assume this has something to do with scrolling..
function goToToday() {
var workbook = SpreadsheetApp.getActiveSpreadsheet(),
currentSheet = workbook.getActiveSheet();
if ( currentSheet.getName() !== "Calculations" ) {
var lastCell = currentSheet.getRange(
currentSheet.getMaxRows(),getMaxColumns()
);
currentSheet.setActiveRange("A1");
var today = new Date(),
rowNumber = 4,
numOfRows = currentSheet.getLastRow() - 3,
dates = currentSheet.getRange(4,1,numOfRows,1).getValues();
for(i = 0; i < numOfRows; i++) {
if(dates[i][0] > today) {
break
}
else {
rowNumber++
}
}
currentSheet.setActiveRange(lastcell);
SpreadsheetApp.flush();
currentSheet.getRange(rowNumber, 1).activate()
}
}
I refactored some function calls, but the main idea is to set the active range to be the bottom right cell, flush the sheet (which should update the UI) and then set the active range to the target.
EDIT: In refactoring the OP's code, I moved the call to .getName() into the conditional, and just stored the sheet in currentSheet. This eliminated multiple calls to worksheet.getSheetByName(currentSheet). Not a really big deal here, but in a larger script, run time would suffer. And, IMO, it's easier to read.
I don't think they are methods in Google Apps Script that can control the scroll, you can request a new feature.
A workaround (not the best one) is to use hideRows() and inmediately call showRows() that will make the UI "scroll". An example assuming the first row is frozen:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
sheet.hideRows(2, 199);
sheet.showRows(2, 199);
sheet.getRange(200, 2).activate();
}
Adapting HardScale's solution, I got this to work. I'ts not the most well written and I'm sure there are plenty of ways to 'thin in out' so to speak, but it works!
function goToToday() {
var workbook = SpreadsheetApp.getActiveSpreadsheet()
var currentSheet = workbook.getActiveSheet().getName();
if (currentSheet !== "Calculations") {
var lastCell = workbook.getSheetByName(currentSheet).getRange(workbook.getSheetByName(currentSheet).getMaxRows(),workbook.getSheetByName(currentSheet).getMaxColumns());
workbook.getSheetByName(currentSheet).setActiveRange(workbook.getSheetByName(currentSheet).getRange(1,1))
var today = new Date()
var rowNumber = 4
var numOfRows = workbook.getSheetByName(currentSheet).getLastRow() - 3
var dates = workbook.getSheetByName(currentSheet).getRange(4,1,numOfRows,1).getValues()
for(i = 0; i < numOfRows; i++) {
if(dates[i][0] > today) {
break
}
else {
rowNumber++
}
}
workbook.getSheetByName(currentSheet).setActiveRange(lastCell);
SpreadsheetApp.flush();
workbook.getSheetByName(currentSheet).getRange(rowNumber, 1).activate()
}
}

Make custom function refresh

So I have this one code that takes a range and a reference cell, and returns the number of cells with the same background color as the ref. This works great, but I want it to be usable when the color of some of the cells changes. I put it on an every minute time based trigger, and I tried to use SpreadsheetApp.flush() to uncache the current number. This did not have the desired affect.
I also tried to make a second function that used the flush() and then returned the first function. This also did not work. The only way I know to make it refresh is to take the "=" from the beginning of the cell with the function in it, and then replace it. The code that works is below.
function countBGColor(range, ref) {
var sheet = SpreadsheetApp.getActiveSheet();
var color = sheet.getRange(ref).getBackground();
var range = sheet.getRange(range);
var rangeVal = range.getValues();
var count = 0;
var allColors = range.getBackgrounds();
for (var i = 0; i < allColors.length; i++) {
for (var j = 0; j < allColors[0].length; j++) {
if (allColors[i][j] == color) count += 1;
};
};
return count;
}
I couldn't find direct way to do this. But you could make script that clears formula and then reenters it into cell:
function Refresh() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange('D1');
range.clear();
SpreadsheetApp.flush();
range.setFormula('=countBGColor("A1:A10","E1")');
}
Next link your image to a script's function: Insert > image > in top right menu of image, Assign Script
Sample file:

Set Charts.newCategoryFilter() to be pre-filtered

I have a script which takes all of the data from a Google Sheet and displays it in a table. I've included several category filters in this so that the user is able to select which data they want to see.
However, I am now looking to take this even further my pre-filtering one of my columns so that it only displays results that are "Closed". I would then create a copy of this file and change it to only showing "Open" results.
Is this possible to do? See a simplified version of my script below. For reference, it's the outcomeFilter that I'm wanting to pre-filter.
function doGet() {
var ss = SpreadsheetApp.openById('spreadsheetkey');
var sheet = ss.getSheetByName('Sheet1')
var data = sheet.getDataRange().getValues();
var dataTable = Charts.newDataTable();
for( var j in data[0] )
if (data[23][j] != 'Closed') {
dataTable.addColumn(Charts.ColumnType.STRING, data[0][j]);
for( var i = 1; i < data.length; ++i )
dataTable.addRow(data[i].map(String));
var stageFilter = Charts.newCategoryFilter().setFilterColumnIndex(21).build();
var outcomeFilter = Charts.newCategoryFilter().setFilterColumnIndex(23).build();
//var testFilter = Charts.newCategoryFilter().setFilterColumnIndex(24).build();
//var testFilter2 = Charts.newCategoryFilter().setFilterColumnIndex(25).build();
var tableChart = Charts.newTableChart()
.setDimensions(1900, 2000)
.setDataViewDefinition(Charts.newDataViewDefinition().setColumns([0,1,2,3,4,5,6,7,8,9,10,11/*,12*/,14,15,16,17,18,19,20,21,22,23]))
.build()
;
var dashboard = Charts.newDashboardPanel().setDataTable(dataTable)
.bind([stageFilter, outcomeFilter/*, testFilter, testFilter2*/],[tableChart])
.build();
var app = UiApp.createApplication();
var filterPanel1 = app.createHorizontalPanel();
var filterPanel2 = app.createHorizontalPanel();
var filterPanel3 = app.createHorizontalPanel();
var filterPanel4 = app.createHorizontalPanel();
//var testPanel = app.createHorizontalPanel();
var chartPanel = app.createHorizontalPanel();
filterPanel4.add(stageFilter).add(outcomeFilter).setSpacing(10);
//testPanel.add(testFilter).add(testFilter2).setSpacing(10);
chartPanel.add(tableChart).setSpacing(10);
dashboard.add(app.createVerticalPanel().add(filterPanel1).add(filterPanel2).add(filterPanel3).add(filterPanel4)/*.add(testPanel)*/.add(chartPanel));
app.add(dashboard);
return app;
}
}
This is the absolute bare bones of my script to try and reduce down the size of the question.
I am also aware I've probably not gone about my code in the best way, but I'm still getting used to working with Apps Script.
EDIT - I've managed to make myself a workaround, where I create two sheets, one which is formulated to only show "Closed" entries and the other is formulated to show only "Open" entries. I have then created a quick apps script which sorts the shown records Z-A.
It works and is a good workaround, but it's annoying that I've got to update two files every time I want to make a mistake. - EDIT
Before you get the data, you could sort the data by the column that you want to exclude values from.
Google Documentation Sort a Sheet Range
You already have a For loop iterating through every row of your data, so you could add a condition to stop the loop when it gets to a value that you don't want.
for (var j in data[0] ) {
if (data[columnYouWantToCheck][j] === valueToExclude) {
break; //End this iteration here if row is unwanted. If you sorted the data.
};
};
The break; will kill the loop, so only use that if you presorted the data. You could also set up the condition like this:
for (var j in data[0] ) {
if (data[columnYouWantToCheck][j] != valueToExclude) {
//All your code here for data in include.
};
};
You can create a filter that will by default have the value "Closed" and be impossible to change to another value using
var testFilter = Charts.newCategoryFilter().setAllowNone(false).setValues(['Closed']);

How to replace text in Google Spreadsheet using App Scripts?

I tried to get the value of a range and than remove all points from the cell.
var FILE = SpreadsheetApp.openById("xyz");
var CONTENT = FILE.getSheetByName("Sheet1");
var A1 = CONTENT.getRange("I17").getValue();
A1. replace(".", "");
It gives me that can't find the replace function. Is there any function in Google Apps Script that allows me to replace the string?
If this is an exact copy of your script then you have a space in-between A1. and replace but I assume it is not.
#SergeInsas is right the content needs to be a string for the replace() function to work, so if your trying to replace the . in a decimal number then you can use the toString() method first like below.
var FILE = SpreadsheetApp.openById("xyz");
var CONTENT = FILE.getSheetByName("Sheet1");
var A1 = CONTENT.getRange("I17").getValue();
var A1String = A1.toString().replace(".", "");
Or you can multiply the number to get rid of the decimal but this will depend on how many decimal places you have :)
There is a more powerful, and simpler, method available: TextFinder.
The accepted answer to this question requires an additional step to post the replaced string back to the cell.
The TextFinder method does not need you to write the data back to the cell.
And if you want to search multiple cells, then this method saves you the iterations.
var FILE = SpreadsheetApp.openById("xyz");
var CONTENT = FILE.getSheetByName("Sheet1");
var A1 = CONTENT.getRange("I17");
A1.createTextFinder(".").replaceAllWith("");
I haven't tested it on a large data set but I suspect this would be quite quick.
Edit: I wrote a short tutorial on this.
For some reason, this solution doesn't work for me. Here is my whole code that should replace the '+' symbol with 'nothing'
// I need to replace more occurrences of different strings, so this is just an example..
var ui = SpreadsheetApp.getUi();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRange("G5:G7").getValues();
// this is a loop, to go through multiple cells that may contain the text, that needs to be replaced.
for (var i = 0 ; i<range.length ; i++) {
var le = range.length;
var stri = range[i].toString().replace("+", "");
Logger.log(stri);
}
var msg = ui.alert("Replaced?");
return msg;
Hope this help you
function removeAccents() {
var spreadsheet = SpreadsheetApp.getActive();
var range = spreadsheet.getRange("F3:F");
var data = range.getValues();
for (var row = 0; row < data.length; row++) {
for (var col = 0; col < data[row].length; col++) {
data[row][col] = (data[row][col]).toString().replace(/é/g, 'e');
data[row][col] = (data[row][col]).toString().replace(/ã/g, 'a');
}
}
range.setValues(data);
};
Sharing a very helpful solution from Bannager Bong on this Google Docs Editor Help Forum thread. Made a slight modification to the function so that it accepts arguments for the find, replace values and then added a range argument so that the function can target a specific region. Even so, this method is extremely slow (my sheets have 5k rows).
function Cleanup12m() {
var spreadsheet = SpreadsheetApp.getActive();
//fandr(",", "");
//fandr("\"","");
fandr("�","",spreadsheet.getRange('BA:BA')); //uses specific range
};
function fandr(find, repl) {
var r=SpreadsheetApp.getActiveSheet().getDataRange();
var rws=r.getNumRows();
var cls=r.getNumColumns();
var i,j,a,find,repl;
//find="abc";
//repl="xyz";
for (i=1;i<=rws;i++) {
for (j=1;j<=cls;j++) {
a=r.getCell(i, j).getValue();
if (r.getCell(i,j).getFormula()) {continue;}
//if (a==find) { r.getCell(i, j).setValue(repl);}
try {
a=a.replace(find,repl);
r.getCell(i, j).setValue(a);
}
catch (err) {continue;}
}
}
};
//Revised to apply to a selected range
function fandr(find, repl, range) {
var r= range;//SpreadsheetApp.getActiveSheet().getDataRange();
var rws=r.getNumRows();
var cls=r.getNumColumns();
var i,j,a,find,repl;
//find="abc";
//repl="xyz";
for (i=1;i<=rws;i++) {
for (j=1;j<=cls;j++) {
a=r.getCell(i, j).getValue();
if (r.getCell(i,j).getFormula()) {continue;}
//if (a==find) { r.getCell(i, j).setValue(repl);}
try {
a=a.replace(find,repl);
r.getCell(i, j).setValue(a);
}
catch (err) {continue;}
}
}
};