Row Protection, after input, when sheet is shared with non google users - google-apps-script

For the scheduling of tennis matches, we have created a sheet in which 170 duo-participants can fill in their names. This sheet is accessible to everyone (so you do not even have to have a Google account)
However, we are looking for the appropriate script to protect the cells that are filled in against possible changes.
Currently a protect is set up when a cell is edited.
But unfortunately, the protect does not work if it is filled in by a non-logged-in person. The protection is made, but not applied.
Who can help us customize the script, so that once a day (eg midnight) all filled cells are protected by anyone (by trigger)
I have set up a copy that is accessible to those who can help.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var Sheet = ss.getSheetByName("PLANNING");
var Range = Sheet.getActiveRange();
var Row = Range.getRowIndex();
var Reeks = Sheet.getRange("D"+ Row);
if (Reeks.getValue() != "") {
if (Reeks.getValue() != "FOUT") {
Range.setNote('geboekt op: ' + new Date());
var LockRange = Sheet.getRange(Row, 6, 1, 2);
var protection = LockRange.protect().setDescription('Row ' + Row + ' Protected');
protection.removeEditors(protection.getEditors());
}
}
}

Instead of a simple trigger use an installable trigger.
Change the name of your function name onEdit to something else like protectOnEdit
Add the installable trigger to run protectOnEdit (or whatever you named your function)
The above because simple trigger function can't execute methods that require authorization like removeEditors

I'm trying to find the reason why the script does not work. Setting up a trigger does not seem to work anyway.
I think it has to do with the other protections that arise. The sheet was originally protected with the EXCEPTION of a specific range. If the script wants to secure a row in that specific range, both statements go against each other.
I will try to figure it out further in the coming days

Related

Google sheets. Lock range

I have 2 lists with ranges C2:F21, which should be available to fill for all users in a certain period.
Range C2:F21 in the list A shouldn't be locked from 01.01.2020 to 05.01.2020, but in another time it should be locked for all users except me.
In the list B range C2:F21 shouldn't be locked only from 01.02.2020 to 05.02.2020.
I will be very grateful for any answer
The most "secure" way to handle this would be to actually save the data as a separate spreadsheet (only you have permission to edit) on the expiration date(s) which you could also automate with a script. You could then use a series of importrange function(s) to create a list of read only information if you need the users to see it in the master sheet. Anyone who is an editor of the spreadsheet has the ability (perhaps not the knowledge) to get around your cell protection in various creative ways.
If you still insist on using cell protection to accomplish this it is very simple, use the Protection class and add a trigger to the project using the little clock in the script editor, Select event source Time-driven and time based trigger Specific date and time.
First you would need to protect the ranges, something that you can do easily through the web UI by selecting the range you want to protect, right-clicking and selecting Protect range. You can do it too via Apps Script, by running a function like this one:
function protectRangeA() {
var ss = SpreadsheetApp.openById("your-spreadsheet-id"); // Change accordingly
var sheetA = ss.getSheetByName("your-sheet-name"); // Change accordingly
var protection = sheetA.getRange("C2:F21").protect();
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
}
This function gets a sheet with a certain name from a spreadsheet with a certain id (as others said before me, for security reasons - to avoid people messing with your script - it would be better if it was a standalone script, not bound to your spreadsheet), and protects the range C2:F21, using protect(), and you will be the only editor.
Second, you want to unprotect this range at a certain date, an protect it again at another date. To do that, you could create time-driven triggers. You could, for example, use an atDate trigger, that will run the function you specify at the year, month and day you specify. So, if you want to unprotect a range at 01.01.2020, you could, for example, have this function:
function createUnprotectTriggerA() {
ScriptApp.newTrigger("unprotectRangeA")
.timeBased()
.atDate(2020, 1, 1)
.create();
}
This will fire a function called unprotectRangeA near midnight of 01.01.2020 (+/- 15 minutes). The function that will be run at this time, then, should unprotect the desired range. It could be something on the following lines:
function unprotectRangeA() {
var ss = SpreadsheetApp.openById("your-spreadsheet-id"); // Change accordingly
var sheetA = ss.getSheetByName("your-sheet-name"); // Change accordingly
var protections = sheetA.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}
}
This function uses getProtections to get all range protections that exist in the sheet and, before checking if the current user can edit them via canEdit(), it removes these with the use of remove().
And to protect it again at 05.01.2020, have this trigger, which will fire the function protectRangeA (previously defined) at this date:
function createProtectTriggerA() {
ScriptApp.newTrigger("protectRangeA")
.timeBased()
.atDate(2020, 1, 5)
.create();
}
Finally, because you want to protect/unprotect several ranges and you would need several triggers, you could call all the functions that create triggers in the same function, which you would have to run once to set all the triggers you need (I didn't define createProtectTriggerB and createUnprotectTriggerB but it would be the same as with the other range):
function createTriggers() {
createProtectTriggerA();
createUnprotectTriggerA();
createProtectTriggerB();
createUnprotectTriggerB();
}
Please beware that, as CodeCamper said, if users have permission to edit the spreadsheet, they could potentially use a script to mess up with your protections. So, depending on the people who are editing this, maybe you should have a copy of the spreadsheet that only you can access.
I hope this is of any help.

e.range.getA1Notation() unable to track changes caused by formula update

I modified a script provided from this blog
How to have your spreadsheet automatically send out an email when a cell value changes
After some debugging an modifications, I can send emails by manually entering a value at position C7. That is, according to script, if the value is greater than 100, it will send a email to me. That only happens if I type the number manually into the cell.
The problem is, if the value is generated by a formula, then it doesn't work. (Say cell C7 is a formula=C4*C5 where the product value is >100)
After some trial-and-error, I think it is the code in the edit detection part causing the problem.
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
Since cell C7 is a formula, the formula itself doesn't change, what is changing is the values from formula calculations. So it may not think I have edited the cell.
How should I modify the script, so that the script also send email when value of C7 produced by a formula is greater than 100?
For reference, here is the code that I am using.
function checkValue(e)
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
{
if(valueToCheck >100)
{
MailApp.sendEmail("h********#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
}
}
}
onEdit(e) Trigger(Both simple and Installable) will not trigger unless a human explicitly edits the file. In your case, Your seem to be getting value from an external source (specifically, Google finance data).
Script executions and API requests do not cause triggers to run. For example, calling FormResponse.submit() to submit a new form response does not cause the form's submit trigger to run.
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause the spreadsheet's onEdit trigger to run.
Also, for Google finance data,
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you will see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
Notes:
Having said that,
In cases where the change is made by a formula(like =IF(),=VLOOKUP()) other than auto-change formulas(like =GOOGLEFINANCE,=IMPORTRANGE,=IMPORTXML, etc), Usually a human would need to edit some other cell- In which case, You will be able to capture that event(Which caused a human edit) and make changes to the formula cell instead.
In cases where the change is done from sheets api, a installed onChange trigger may be able to capture the event.
To avoid repeated notifications due to edits in other places, send an email only when the value you are tracking is different from the previous one. Store the previous value in script properties.
function checkValue(e) {
var sp = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var oldValue = sp.getProperty("C7") || 0;
if (valueToCheck > 100 && valueToCheck != oldValue) {
MailApp.sendEmail("***v#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
sp.setProperty("C7", valueToCheck);
}
}
Here the email is sent only when valueToCheck differs from the stored value. If this happens, the stored value is updated by setProperty.

On Change Trigger Not Running Script On Change

I have a script to update named ranges when new rows of data are added to the spreadsheet in question:
function updateNamedRanges() {
// get to the right place
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('ga weekly data pull');
//now update the named ranges if they have changed in length
var openEnded = ["gaCampaign", "gaMedium", "gaSource", "gaSubscriptions", "gaUsers", "gaWeek"];
for(i in openEnded) {
var r = ss.getRangeByName(openEnded[i]);
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue; // ok as is-skip to next name
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(openEnded[i]);
ss.setNamedRange( openEnded[i], s.getRange(rfr, rfc, rnr, rnc ));
}
sheet.getRange("D2").setValue(0); // this gets all the formulas in the sheet to update - just changing any cell
}
Then, within Aps Script editor I go Resources > Current Projects Triggers > Run updateNamedRanges > From Spreadsheet > On change.
Now, if I manually add in a row of data the script runs - great!
But I'm pulling in data with the Google Analytics add on. This add on expands the tab in question when the length of data is longer than the sheet. But when this happens the script does not update.
Is there anything I can do here?
As a backup I'm thinking if I can figure out how to get GAS to add a row from the bottom of the sheet that might do it but that seems like a workaround. Before I go down that path is there a better way?
as you found out, apps script triggers only work when apps script does the changes. yea its lame. if an api outside of apps script modifies the sheet, they wont trigger.
your only option is to use a time trigger to detect a change and process the entire sheet again (since you dont know what changed). One way to achieve this more efficiently is to remember (in a script property) the last modified date from triggers. then a 1minute time trigger checks if modified date is now bigger than the last one saved. if so process the entire sheet.
Run it on a time trigger that runs every minute until Google addresses the issues of not catching the on change event and/or not being able to define open-ended named ranges.
Edited for running the script on open
To keep the sheet from recalculating everytime it is opened whether needed or not.
above the loop place:
var recalc = false;
within the loop below if(rlr==slr ) continue;
recalc = true;
recalculate the sheet only if necessary:
if(recalc) {sheet.getRange("D2").setValue(0)};

GAS - Ranged Cells Protection

I noticed there is quite a number of questions here regarding protection on cells in a spreadsheet.
But there seems to be no viable solution.
For example, column 'A' can only be edited by person1#email.com, and column 'B' can only be edited by person2#email.com.
There seems to be an issue tracker on google site since 2013...but Google has not come up with an API for it yet.
Does anyone have a workaround?
The code below only works for entire page protection..
sheet.setSheetProtection(permissions);
Use an onEdit() function that checks what user is editing the Sheet, then check what column is being edited. Have an object of user names, and what columns they can edit. If a user is not allowed to edit, undo the change.
You can only undo the change if you have a way of knowing what the last cell value was. There is no undo method in Apps Script, or other built in way to get the old value with Apps Script. But there is a way to configure the data to achieve a way to undo the edit.
Have a central sheet with all formulas referring to other sheets. In other words, the data that people view is a copy of the stored data in another sheet. Divide the data into sheets according to who can edit what. The code will write data to the correct sheet when a cell is edited.
Basically, you would have sheets that are the database where the data is stored. Those sheets could even be hidden, and of course they would be protected.
The viewing and editing would be done in a separate sheet from the sheets that are the official data storage.
So, the sheet that people are viewing and editing is the "User Interface"; it's the "Front End" of the "App". The sheets that are the official data storage are the "Back End".
function onEdit(e){
Logger.log("e.value: " + e.value);
Logger.log("e.range.getRow: " + e.range.getRow());
Logger.log("e.range.getColumn: " + e.range.getColumn());
var objWhoCanEditWhat = {"user1":"[A,B]", "user2":"[A]"};
//Get this user
var thisUserIs = Session.getActiveUser().getEmail();
Logger.log('thisUserIs: ' + thisUserIs);
Logger.log('Index of #: ' + thisUserIs.indexOf("#"));
thisUserIs = thisUserIs.substring(0, thisUserIs.indexOf("#"));
Logger.log('thisUserIs: ' + thisUserIs);
var whatColumnCanEdit = objWhoCanEditWhat[thisUserIs];
Logger.log('whatColumnCanEdit: ' + whatColumnCanEdit);
var editedColumn = e.range.getColumn();
var editedRow = e.range.getRow();
Logger.log('editedColumn: ' + editedColumn)
var ss = SpreadsheetApp.getActiveSpreadsheet();
//There must be a way to determine what sheet needs to be accessed, and that sheet name
//is set dynamically.
var objColumnEditedToSheetName = {"ColA":"Sheet6TY", "ColB":"SheetColumnB"};
var whatSheetToUse = objColumnEditedToSheetName[editedColumn];
if (whatColumnCanEdit != editedColumn) { //If the column this user can edit is not the same as
//the column that just was edited, then
//Undo the change with this code
//Retrieve the old official data from the data storage sheet
var sheet = ss.getSheetByName(whatSheetToUse);
} else {
//If the user is allowed to edit this column, write the data to the official data storage sheet
var sheet = ss.getSheetByName(whatSheetToUse);
};
//Always put a formula back into the cell that was just edited in order
//to show data from the back end data source
var viewSheet = ss.getSheetByName("SheetForEditingAndViewing");
//You know the row and column of the cell that was just edited, so use that to
//reference what cell to put the formula back into.
viewSheet.getRange(editedRow, editedColumn).setFormula("Sheet1!A3");
};

Why doesn't the trigger work for the script running a function on my google spreadsheet?

So I'm using a script on my googledoc spreadsheet that goes through a column and counts a certain number of occurrences of specified formatting. (i.e. in my spreadsheet "=myFunction()")
The function works fine, but my problem is despite setting up a trigger to run the script "onEdit", it never does it. I have to open the script up and save it each time I want it to update on my spreadsheet.
I've been looking up for hours but no one seems to have my question. There are no errors sent by the notifications. The code (though I don't think it's terribly relevant) for my function is:
function CountIfNotStrikeThrough2()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mysheet = ss.getActiveSheet();
var mydatarange = mysheet.getRange(1,1,390,1);
var numRows = mydatarange.getLastRow();
var rowindex = mydatarange.getRowIndex();
var columnindex = mydatarange.getColumnIndex();
var total =0;
for(i=rowindex;i<=numRows;i++)
{
if(mydatarange.offset(i-1, columnindex-1, 1, 1).isBlank() != true && mydatarange.offset(i-1, columnindex-1, 1, 1).getFontLine() != "line-through")
{
total++;
}
}
return total;
}
I know there's a thread here that has a comprehensive list of when the onEdit trigger doesn't activate. (here) For example, if you setup Data Validation and select an item from the list of values in the validation, it doesn't trigger the onEdit function.
Besides that, could you try a simple trigger instead of installable? This is to say, instead of explicitly associating CountIfNotStrikeThrough2() to the onEdit trigger, please try re-naming the function to "onEdit()" instead and see if that works. This creates an implicit or simple trigger. I've had trouble in the past with installable vs. simple triggers.
Also, please share the exact action you're performing to test the trigger.
Reference: https://developers.google.com/apps-script/understanding_triggers