Use Case - I have shared a Google Spreadsheet amongst a dozen friends and we are entering our prediction for matches. The catch is to enter it before the game starts.
Using Spreadsheet as everyone can see everyone's prediction.
Problem - Is there an AddOn or any feature which allows to disable editing a few cells after a particular time? Say post midnight A[7]-M[7] cells cannot be edited.
You can set a script to run at a specific time:
In the script editor, create a function:
function protectRangeAtMidnight() {
//My code will go here
}
In the script editor, click on the RESOURCES menu, and choose, CURRENT PROJECTS TRIGGERS.
Add a trigger for a specific date and time.
The problem is, what is the code going to do? If you protect the range, but the people you are sharing the spreadsheet with have edit authority, they can just unprotected the range. If you change their permissions to VIEW only, then you'd have to change it back at some point for the next game. That would work as long as there can be a time period where no one else can edit the sheet.
You can remove a user from the editor list:
Remove Editor
function protectRangeAtMidnight() {
SpreadsheetApp.openById('The SS ID').removeEditor(emailAddress);
}
You can also set file sharing permissions through DriveApp:
setSharing(accessType, permissionType)
I am actually doing exactly the same thing and came across the same problem. The solution I came up with does not lock the cells but uses data validation. Some of the solutions suggested online did not seem to take into account that you need to lock a row of results which have a date associated with it.
This is the layout I am using for my predictions:
Google sheet cropped image example
The cells in blue then have the following data validation (criteria is custom formula, reject input):
=if(isnumber(C1),and(now()<$A1,C1>=0,C1-int(C1)=0))
It checks that what is entered in C1 is a number. If it is, it then checks the following:
If the current date and time is before 'kick off'.
If the number is greater than or equal to zero.
That it is a whole number.
If so, it allows the cell to be changed. If the match has kicked-off, the cell cannot be altered and a red triangle will appear in the cell (because the data validation will be violated as now() will be after the date in question) but it stops the cells from being changed once the game has kicked off.
If you couple the above with locking the entire sheet (apart from the blue cells) it should allow users to make predictions prior to kick-off.
If it is necessary for cells to be altered after the game has begun you can modify the date in column A to then make the update before changing the date back.
Hope this helps!
What works is to:
Lock the cells or columns at a specific time, then remove the protection at another time: using a daily trigger (or even manually)
Modifications needed before running the functions:
Sheets names (In my example it's Sheet1, Sheet2)
Range to protect (in my example it's A:D) (editors won't be able to edit the specified range)
function Lock() {
var tabs = ['Sheet1', 'Sheet2'];
var ss = SpreadsheetApp.getActiveSpreadsheet();
for (var i = 0; i < tabs.length; i++) {
var spreadsheet = ss.getSheetByName(tabs[i]);
var protection = spreadsheet.getRange('A:D').protect();
protection.setDescription('Protected')
protection.removeEditors(protection.getEditors());
}
};
And to remove the protection you'll use the following script: Editors get "back" their permission to edit the specified range:
function Unlock() {
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
if (protections[i].getDescription() == 'Protected') {
protections[i].remove();
}
}
};
Related
I've recently started working with Apps Script to improve the scope of what my google sheets can do, and I wanted to ask more experienced people how I might make my script more efficient. I used a mixture of tutorials, documentation, and trial & error to make it. I find that although it usually completes the task it's meant for, sometimes it takes an unreasonably long time or exceeds its runtime and simply stops.
I would like to know which best practices I could implement to make it run more quickly overall, and which things I might be able to include in future scripts to avoid any pitfalls I'd landed in here.
Scope:
The script is meant to take each day's new data and apply it to a new sheet called 'TODAY.' It works as follows.
Rename the tab labeled 'TODAY' to the previous workday's date (if today is 2.3, it renames the sheet to 2.2.)
Hide this renamed tab.
Duplicate the 'TEMPLATE' tab, and rename it to 'TODAY.'
Pull data from the 'RAW DATA' tab, and paste it into the new 'TODAY' tab.
Paste a formula into the new 'TODAY' tab and drag it down to the bottom of the table so that the correct values populate and the conditional formatting occurs.
Any help would be greatly appreciated, I really just need some direction for how to improve my work.
Here is a link to an example sheet with editing permissions enabled: https://docs.google.com/spreadsheets/d/1F7bAd2DjKgk53e-haPgjWfFphMfu5YBn8iRQ3qwC3n0/edit?usp=sharing
In my humble opinion, a good Google Sheet App Script doesn't need to use activate to control the source or destination of data. The sheet and script developer should know what and where they want the data to come from and go. Activate is like using the mouse to click on something.
I've taken your script and rewritten to minimize the use of variables. I have only one sheet variable and reuse it throughout. In fact for the majority of the time it is the copy of the TEMPLATE called TODAY.
Also unless I have to use a sheet last row many times, I avoid using a variable and instead just use sheet.getLastRow(). Same for columns.
I always wrap my code in a try catch block as a matter of habit.
As a last note, unless you change the notation in column C and N you could have used your script to fill in column B.
function myDailyUpdate() {
try {
let spread = SpreadsheetApp.getActiveSpreadsheet();
// Step 1
let sheet = spread.getSheetByName("TODAY");
let oldDate = sheet.getRange("Q4").getValue();
let prevDate = Utilities.formatDate(oldDate,"GMT-5","M.d");
// Renames old 'TODAY' sheet to previous workday's date.
sheet.setName(prevDate);
// Sets the color to red.
sheet.setTabColor("990000");
// Hides the old 'TODAY' sheet
sheet.hideSheet();
// Step 2
sheet = spread.getSheetByName("TEMPLATE");
// Copies the contents of the 'TEMPLATE' sheet to a new sheet called 'TODAY.'
sheet = sheet.copyTo(spread);
sheet.setName("TODAY");
sheet.activate(); // required to move to 1st position
// Move TEMPLATE to first position
spread.moveActiveSheet(1);
// Step 3
// Colors the 'TODAY' tab green to signify it being active.
sheet.setTabColor("6aa85f")
// Identifies the 'RAWDATA' sheet for later use.
let source = spread.getSheetByName("RAWDATA");
// Identifies ranges and values for later use.
let values = source.getDataRange().getValues();
// sheet is still the "TODAY" sheet
// Identifies 'TODAY' sheet as the recipient of 'RAWDATA' data, and identifies the range.
// Sets the values from 'RAWDATA' into 'TODAY.'
sheet.getRange(12,2,values.length,values[0].length).setValues(values);
// Step 4
// sheet is still the "TODAY" sheet
let range = sheet.getRange("C12");
range.setFormula(
'=IFERROR(IFERROR(IFS(VLOOKUP($B12,INDIRECT'
+'('
+'"'
+'\''
+'"'
+'&$Q$4&'
+'"'
+'\''
+'!"&"!"&"A1:O2000")'
+',15,false)="D","D",$N12="Quote","Q",$N12="Important","I",$N12="On Hold","H",$N12="IN TRANSIT","T",$N12="REQUEST","R",$N12="INCOMPLETE","N",$N12="COMMENT","C"),VLOOKUP($N12,$B$3:$C$9,2,FALSE)),"")');
// Pastes the above formula into cell C12.
let fillRange = sheet.getRange(12,3,values.length,1);
range.copyTo(fillRange);
sheet.activate();
}
catch(err) {
console.log(err);
}
}
I made a Google Sheet to check every media that plays on a certain channel on TV using a lot of workaround formulas within the cells themselves. A part of this sheet is a column (G) that tells me whether or not the specific episode/media/whatever is currently playing, has played in the past or will be played later today/at a later date using a "NOW" function. Next to that column there is another (F) where the user is able to write a "V", and in the case the show is playing but the user hasn't checked it yet, it writes "Check Me" See Example.
I wanted to create a button that will automatically change that "Check Me" into a "V" but the problem is that "Check Me" is based on a simple formula written throughout column F (=IF(G5="Playing","Check Me","")), so when I tried to run a script I found here on StackOverflow:
function Test() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("F5:F700");
range.setValues(range.getValues().map(function(row) {
return [row[0].replace(/Check Me/, "V")];
}));
}
(Can't remember the exact thread I got it from and it's been two days since I found it with lots of similar searches in my history, so I apologize for not crediting)
together with its intended use, it also straight up deleted all the rest of the formulas from the column, probably due to the formula itself containing "Check Me" but I might be mistaken.
To be honest, before this week I barely ever worked with either Google Sheets, much less JavaScript or even coding in general, so I'm pretty much restrained to changing values and very minor modifications in scripts I find online.
The only idea I had as to how to solve it is to add an "if IsBlank" but regarding face value of the cell only rather than its contents, but I don't know how to do it or whether it is even possible in the first place. At the very least, google shows no results on the subject. Is there a way to add that function? or perhaps a different method altogether to make that button work? (it's a drawing I will assign a script to)
Because you're using a map function to update the range, you'll need to get the formulas using getFormulas() in addition to the display values using either getValues() or getDisplayValues(). Using only the display values, as you're currently doing, will cause you to lose the formulas when you update the sheet. Conversely, using only the formulas would cause you to lose all of the display values that don't have a formula, so you'll need both. Try this and see if does what you want:
function Test() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("F5:F700");
// Get all cell formulas and display values
var formulas = range.getFormulas();
var values = range.getValues();
range.setValues(formulas.map(function(row, index) {
var formula = row[0];
var value = values[index][0];
// Check only the display value
if (value == "Check Me") {
return ["V"];
} else {
// Return formula if it exists, else return value
return [formula || value];
}
}));
}
I have no experience with scripting in Excel or Google Sheets, so I'm trying to branch out a bit and see if there's a solution to my problem. We use Google Sheets for a weekly calendar at our kitchen remodeling business. We organize the weeks from left to right and list the jobs we're currently working on in those columns. I would like to automatically hide all columns that have a date older than 4 weeks, so when the sheet opens, we're not starting with a date from a year ago. I can hide these columns manually each week, but when I do need to go back and look at previous weeks, I'm forced to unhide all thosecolumns and then highlight all of the columns I want to rehide. Having a script seems like the better solution.
Is there a way to have a script run every time the file is open so that we're always only displaying the previous 4 weeks and everything in the future? If so, would you be willing to help me understand how I might write that and get it working? Again, I'm a novice when it comes to anything beyond formulas, but very interested in learning more about the scripting capabilities.
Thank you!
With Apps Script via Tools->Script Editor, you could create a menu with an onOpen() function. The function in the menu (e.g. hidePast), would then need to check a given value in each column (to see what date that column refers to), and then flag it to be hidden or not. The onOpen function, because it is a "simple trigger", cannot do anything that requires "authorization" (such as interacting with non-local Spreadsheet data), hence the intermediate method. By creating a menu, you can make it easy for anyone using the spreadsheet to authorize and activate the function.
Example:
/* #OnlyCurrentDoc */
function onOpen() {
SpreadsheetApp.getActive().addMenu("Date Tools",
[{name:"Hide Past", functionName:"hidePast"},
{name:"Show All", functionName:"showAll"}]);
}
function showAll() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
sheet.unhideColumn(sheet.getDataRange());
ss.toast("All columns unhidden.");
}
function hidePast() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
// Acquire the 1st row of all used columns as an array of arrays.
var datelist = sheet.getSheetValues(1, 1, 1, sheet.getLastColumn());
// Drop the hours, minutes, seconds, etc. from today.
var now = new Date();
var today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
// Inspect the datelist and compare to today. Start from the rightmost
// column (assuming the dates are chronologically increasing).
var col = datelist[0].length;
while(--col >= 0) {
var then = new Date(datelist[0][col]);
if(then < today) {
break;
}
}
// Bounds check, and convert col into a 1-base index (instead of 0-base).
if(++col < 1) return;
// col now is the first index where the date is before today.
// Increment again, as these are 2-column merged regions (and
// the value is stored in the leftmost range). If not incremented,
// (i.e. hiding only part of a merged range), spreadsheet errors will occur.
sheet.hideColumn(sheet.getRange(1, 1, 1, ++col));
ss.toast("Hid all the columns before today.");
}
Because you don't have a "database like" source it will be very difficult, but you can try to create a very complicated
QUERY()
you should filter the dates in another sheet (and you may face a dead end).
So I will suggest using this kind of structure and it will also allow you to make other kinds of filters (or Pivot Tables) in the future (maintainable and scalable).
Thank you very much in advance for your help, I'm new to coding but proficient with standard Excel functions. I would greatly appreciate any input on this project.
I want to create a Google spreadsheet that has 3 sheets. The first is a DATA sheet which lists stock tickers and provides live prices via Google finance {=googlefinance(VTI,price)}.
The second sheet is the MASTER sheet that aggregates all of the positions, including number of shares in stocks, quantity in fixed income instruments, quantity in bullion, etc. The prices used to calculate current market value of positions are drawn from the DATA sheet. All values are added together to create a total value cell, E57, that updates itself automatically from the google finance data throughout the trading day. This all works fine.
The final sheet is the HISTORY sheet. Here's what I want to do. I want cell E57 to copy to this sheet once a day at market close so I have a daily history of the aggregate portfolio. Each time the script copies and pastes the value to the HISTORY sheet, it needs to paste on the next available row in the same column. So far, I've written a script that successfully copies and pastes the value at a defined time interval (using Project Triggers), but it just keeps pasting over the previous value. How can I make it paste to each successive open cell and generate a list?
Also, I need all of this to work without me signing in or opening/activating the sheet. I want it to run completely autonomously, that's why I'm activating the sheet via openById instead of using the ActiveSheet code (I think that reasoning is correct, but not sure).
Here's the script I have:
function PasteValue() {
var ss = SpreadsheetApp.openById("0Ao2pCtssx6TcdGpDWFpSXy1pUXA3MlAtSjZFVHlaZVE");
ss.getRange("MASTER!E57").copyTo(ss.getRange("HIST!C5"),{contentsOnly:true});
}
What do I do to improve?? Thank you!
Your code suggests the name of the history sheet is HIST.
I think there are a few ways to do this:
Option 1:
Uses the getLastRow() method
function PasteValue() {
var ss = SpreadsheetApp.openById("0Ao2pCtssx6TcdGpDWFpSXy1pUXA3MlAtSjZFVHlaZVE");
var nextRow = ss.getSheetByName('HIST').getLastRow()+1;
ss.getRange("MASTER!E57").copyTo(ss.getRange("HIST!C" + nextRow),{contentsOnly:true});
}
Note that option 1 will not work if you have a column of cells that contain formulas that have been copied down to the bottom of the sheet.
Option 2:
Loops through the cells in column C to find the first blank cell.
function PasteValue() {
var ss = SpreadsheetApp.openById("0Ao2pCtssx6TcdGpDWFpSXy1pUXA3MlAtSjZFVHlaZVE");
var values = ss.getRange("HIST!C:C").getValues();
for (vari=0; i<values.length; ++i) {
if (values[i][0] == "") {
var nextRow = i+1;
break;
}
}
ss.getRange("MASTER!E57").copyTo(ss.getRange("HIST!C" + nextRow),{contentsOnly:true});
}
Option 2 should work as long as you have no blank cells in column C.
Option 3:
Use the appendRow() method
Good luck!
I am programming a help desk system using google script, forms and spreadsheet.
To filter the queries the submissions are placed into different sheets depending on category, this is done through the FILTER function. however every time a new submission is made the filter function does not update, (it uses the CONTINUE function to cover the other cells)
instead the cell with the FILTER function must be selected and crtl+shift+E must be entered
is there a way around this?
I have tried two methods
the first was looking to have a function to enter the shortcut, but is this possible?
the second is auto entering the continue function everytime a new submission is made, I have this working however google sheets does not recognise the named range, (the continue function has the set up CONTINUE(original cell, rows away, columns away) its the original cell that it does not identify, instead I must manually select the cell and re-write the exact same cell reference.
Thank you for your help, if you need to see my code please ask :)
This is the code for the second option where I try to enter the function manually to the cells.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var numEntry = ss.getSheetByName('Home').getRange("B8").getValue() + 2;
var cat = ss.getSheetByName('Software problem').getRange(numEntry, 4, 1, 9);
cat.getCell(1, 1).setValue('=CONTINUE(D2, '+(numEntry-1)+', 1)');
Your option 1: Have a script enter keystrokes automatically? Not supported in apps-script.
Your Option 2: It shouldn't be necessary to programmatically insert CONTINUE, as the required CONTINUEs for your FILTER should be automatic, when rows in your filter range match the expressed criteria. Something else is wrong, so don't get caught up with this red herring.
You mention "google sheets does not recognise the named range" - I'd like to know what you mean by that, because I suspect this is where your solution will be. You can use named ranges within FILTER statements. You can also use open-ended ranges, like FormInput!A1:X or FormInput!E1:E.
If you're trying to manipulate named ranges using scripts, then you may have run into a known issue, "removeNamedRange() only removes named ranges that were created via Apps Script". (To get around that, manually delete the named range, then create it only from script.)
Here's a function I use to create a named range for all data on a sheet. You could adapt this to your situation. (I use this with QUERY functions instead of FILTER, you might want to consider that as an alternative.)
function setNamedRangeFromSheet(sheetName) {
// Cannot remove a named range that was added via UI - http://code.google.com/p/google-apps-script-issues/issues/detail?id=1041
var ss = SpreadsheetApp.getActiveSpreadsheet();
try { ss.removeNamedRange(sheetName) } catch (error) {};
var sheet = ss.getSheetByName(sheetName);
var range = sheet.getDataRange();
ss.setNamedRange(sheetName,range);
}
Using FILTER, you need to match the length of your sourceArray (which can be a named range) and any criteria arrays you use. To programmatically create a named range for a single-column criteria within your sourceArray, and of the same length, use getNumRows() on the sourceArray range.
Now, within your submission handling function, triggered on form submit, you'd have something like this. (I assume your trouble reports are coming into a single sheet, "FormInput" - adjust as necessary.)
...
var ss = SpreadsheetApp.getActiveSpreadsheet();
try { ss.removeNamedRange("FormInput") } catch (error) {};
var sheet = ss.getSheetByName("FormInput");
var inputRange = sheet.getDataRange();
ss.setNamedRange("FormInput",inputRange);
try { ss.removeNamedRange("Criteria") } catch (error) {};
var criteriaCol = 4; // Another guess, that Column E contains our criteria
var criteriaRange = sheet.getRange(0,criteriaCol,inputRange.getNumRows(),1);
ss.setNamedRange("Criteria",criteriaRange);
...
And with that in place, the content of A1 on your "Software problem" sheet just needs to contain the following. (Assuming that you're looking for "Bug"s.):
=FILTER(FormInput,Criteria="Bug")
I mentioned open-ended ranges earlier. If you aren't doing enough manipulation of data to justify named ranges, you could set up your filter like this, and not have to change it as new input came in:
=FILTER(FormInput!A1:X,FormInput!E1:E="Bug")