GAS - Ranged Cells Protection - google-apps-script

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");
};

Related

Getting email notification via Google Sheets when =importxml cell value changes

I'm working on a spreadsheet that uses multiple =IMPORTXML functions to import changing text and price values from a webpage. At this moment I have the following columns in my Google Sheet:
A: 'URL info'
B: 'URL'
F-N: 'Price' (in every column a different price value)
What I have
Via a script, found on this page (thank you Umesh Agarwal) I will receive an email notification once a change has been made within the spreadsheet. Once I make a change in a cell within the range of F2:N200 I will receive an email with the cell that have been changed. The problem is that I have the script to sent me an email with the changed cell once the cell with a value of the =importxml function is changing.
At this moment, when a cell is changing due to the =importxml function the script is sending me an email that cell A1 has changed... it is not sending me the right cell that has been changed which makes it difficult to see what changed. How can I solve this problem?
function sendEmailonEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cellValue = ss.getActiveSheet().getActiveRange().getA1Notation();
var getColumn = ss.getActiveSheet().getActiveRange().getColumn();
var sheetname = ss.getActiveSheet().getName();
var user = Session.getActiveUser().getEmail();
var Toemail = 'myemailid_1234#gmail.com';
var subject = 'New Entry in ' + data + '.' + ss.getName();
var body = 'Your file has a new entry in - ' + sheetname + ' Updated by - ' + user + data
' check file- ' + ss.getUrl();
if(data.indexOf('F2:N200')!=-1.23456789) {
MailApp.sendEmail(Toemail,subject, body);
}
};
I'm afraid this is not possible with triggers the way you have it set up.
For the trigger to fire when a formula is pulling data from an external source "On change" is the only trigger that will pick up the change. Unfortunately, it won't return which cell or value has changed, it will only return which sheet has changed.
The other alternative trigger you might run into is "on Edit", however, this trigger will not fire when the sheet is updated by formulas pulling data from an external source ¯\_(ツ)_/¯
Avenue for possible workaround:
You might be able to work around this with a Time-driven trigger AKA clock trigger. Writing a script that fires every so often to check for changes in the worksheet, and to send an e-mail if it does. You might copy all the data to another sheet and then compare the values, or use the Properties Service, to persist data within the script.

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

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

Protecting a cell from consecutive POSTs

I am trying to create a time-based/triggered lock on data in cells of a spreadsheet so that the data in the cell is not changed during certain periods of the day. But before I created the time-lock to remove the range protection I need to first lock the cell when the first POST is made to it.
How do I lock a cell from being edited after the first POST so that consecutive POSTs to the same cell during the lock period do not change the data?
Note: The app is executed as me and anyone including anonymous has access to it.
doPost(e)
{
ssNew = SpreadsheetApp.openById(ssId);
var sheet = ssNew.getSheets()[0];
var cell = sheet.getRange(1,1);
var visits = cell.getValue();
cell.setValue(e.parameter.name);
SpreadsheetApp.flush();
var lockrange = sheet.getRange(1,1);
var protection = lockrange.protect().setDescription('Protected');
var me = Session.getActiveUser();
protection.removeEditor(me);
}
You'll want to store a flag in some kind of persistent memory: like using the PropertiesService (probably script properties in this case) or a spreadsheet cell in another sheet. The doPost() could then check if that flag is set before changing it.
You could also look at programmatically protecting the cell against edits by users.

Copying Data Sheet1 to Sheet2 so you can sort & edit both sheets (google apps script?)

I am working in goggle sheets and think I need to use a google apps script to do what I want, but I am a psychologist at a non-profit University hospital trying to do some good and not a programmer (which probably shows) and I am desperately in need of help. I am trying to set up a series of spreadsheets to track participation in workshops for our treatment method.
1) I have a sheet “Participant_Registration” where basic information is entered
2) I want to transfer information from only the first four columns (A:D) of “Participant_Registration” to a second sheet “Learning_Sessions_Attendance”
3) I am also transferring the same information to a third sheet 'Consultation1_Attendance' – but I need to first filter and select only those people assigned to that group.
Here is a link to a copy of my spreadsheet.
https://docs.google.com/spreadsheets/d/17d0bT4LZOx5cyjSUHPRFgEZTz4y1yEL_tO3gtSJ4UJ8/edit?usp=sharing
More generically this is what I am trying to do. Is this possible in google app scripts? It seems it should be.
1) I have original data in sheet1
2) I want the first four columns (A:D) to transfer to sheet2 (it is fine if I need a trigger variable)
3) I want them to transfer in such a way that if you sort either sheet, the data are still fine (still linked to the right line).
4) Ideally if there is a change to the data in the source sheet (Sheet1) the same change will be made in Sheet2.
5) Ideally this would all happen automatically without human intervention through a script.
Any ideas?? I so need your help. I have been all over the forum, git hub, and done a ton of searches and tried following a lot of examples I saw but nothing works. I really need help.
Here are my sample scripts each with a problem:
//The following code copies a range from sheet1 to sheet2 as I wanted. A problem occurs if after if we copy the data from sheet1 we add data to other columns on sheet2. Later if we sort on some variable (which people are bound to do) if the function is deployed again it will overwrite data meaning the data from sheet1 are not connected to the right individual on sheet2
function CopyRange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Participant_Registration");
var range = sheet.getRange("A14:D");
var values = range.getValues();
var target = ss.getSheetByName("Learning_Sessions_Attendance");
var target_range = target.getRange("A10:D");
range.copyTo(target_range);
}
So I tried again. This time I tried to just copy the last edited row from sheet1 to sheet2. This function does not appear to work for me.
function CopyRow2() {
// Get Spreadsheets
var source = SpreadsheetApp.openById("1egn6pnRd6mKMGuQxX_jtgwYDtkuMUv2QJItLdh7aIEs");
var target = SpreadsheetApp.openById("1egn6pnRd6mKMGuQxX_jtgwYDtkuMUv2QJItLdh7aIEs");
// Set Sheets
var source_sheet = source.getSheetByName("Participant_Registration");
var target_sheet = target.getSheetByName("Learning_Sessions_Attendance");
var rowIdx = source_sheet.getActiveRange().getRowIndex();
var rowValues = source_sheet.getRange(rowIdx,1,1,source_sheet.getLastRow()).getValues();
Logger.log(rowValues);
var destValues = [];
destValues.push(rowValues[0][0]);// copy data from col A to col A
destValues.push(rowValues[0][1]);//copy data from col B to col B
destValues.push(rowValues[0][2]);//copy data from col C to col C
destValues.push(rowValues[0][3]);//copy data from col D to col D
var dest=source.getSheets()[4];
dest.getRange(dest.getLastRow()+1,1,1,destValues.length).setValues([destValues]);//update destination sheet with selected values in the right order, the brackets are there to build the 2D array needed to write to a range
}
So I tried again and again. I have lots of examples but none seem to work.
Thanks so much.
Chandra
For that to happen automatically (one sheet's change updating another sheet), you will surely need an "event/trigger" to run a script whenever you change a cell. (that is the "onEdit()" function).
But since scripts are likely to fail sometimes (even when they are perfect, that's because of some Google issues), it's not guaranteed that the sheets will always contain the same data.
But, if I could suggest another way, do not let ID be optional. If that is a real ID (like the person ID card number), create another ID exclusively for working with the sheet.
I have edited your second sheet showing a suggestion of how to do it without using scripts. The only things you must be aware of are:
Do not create two people with the same ID.
You have to insert (only) the ID manually in the second sheet.
The VLOOKUP forumla will search for that ID in the first sheet and return the data in the same line. You can sort any sheet in whatever way you like. As long as you don't change people's IDs.
So, in sheet 2, use this in the First Name, Last Name and Email address:
=vlookup(A10,Participant_Registration!$A:$D,2,false)
=vlookup(A10,Participant_Registration!$A:$D,3,false)
=vlookup(A10,Participant_Registration!$A:$D,4,false)
Just extend this formula downwards
I hope this helps. I would avoid scripting for that at any cost. It would be my last resort. (Scripts also need to be changed if you want to rearrange your sheet, and if not, they might cause trouble, write over existing data...)
I also added a button (insert - drawing) and put a script in it (right button, click down arrow, "transfer? script" -- translated from Portuguese).
If you lock all four columns in sheet2 and lock the ID column in sheet 1, people will not be able to chang IDs and cause mess. They can edit people in sheet 1 and not change the formula in sheet2. Script is not affected by sorting or empty spaces (it adds the person in the first empty row it finds).
I added "named ranges" for the four column headers. (With named ranges, the script can refer to names instead of coordinates, which enables you to rearrange the sheet inserting and deleting columns, or moving them with CUT and paste - but the VLOOKUP formula will need manual update if you rearrange columns).
Here is the code: (it could get better if you manage to create dialog boxes and ask for the person's data inside that dialog, then you could lock everything - and you would need an edit button besides the add).
function AddPerson()
{
var S1Name = "Participant_Registration";
var S2Name = "Learning_Sessions_Attendance";
var ID1Name = "regID";
var ID2Name = "learnID";
//these vars are not used in this script
var FN1Name = "regFirstName";
var FN2Name = "learnFirstName";
var LN1Name = "regLastName";
var LN2Name = "learnLastName";
var Email1Name = "regEmail";
var Email2Name = "learnEmail";
var sSheet = SpreadsheetApp.getActiveSpreadsheet();
var Sheet1 = sSheet.getSheetByName(S1Name);
var Sheet2 = sSheet.getSheetByName(S2Name);
var ID1 = getRangeByName(sSheet, Sheet1.getName(), ID1Name);
var ID2 = getRangeByName(sSheet, Sheet2.getName(), ID2Name); Logger.log("ID2: " + ID2.getValue());
var Empty1 = getFirstEmpty(ID1);
var Empty2 = getFirstEmpty(ID2);
var Biggest1 = getBiggestID(ID1); Logger.log("Biggest 1: " + Biggest1);
var Biggest2 = getBiggestID(ID2); Logger.log("Biggest 2: " + Biggest2);
if (Biggest1 !== Biggest2)
Browser.msgBox("Warning: there are IDs in one sheet that are not in the other sheet");
var Biggest;
if (Biggest1 > Biggest2) Biggest = Biggest1;
else Biggest = Biggest2;
Biggest++;
Empty1.setValue(Biggest);
Empty2.setValue(Biggest);
}
function getFirstEmpty(Header)
{
while (Header.getValue() !== "")
{
Header = Header.offset(1,0);
}
return Header;
}
function getBiggestID(Header)
{
var Sheet = Header.getSheet();
var LastRow = Sheet.getLastRow();
var Values = Sheet.getRange(Header.getRow(), Header.getColumn(), LastRow - Header.getRow() + 1).getValues();
var len = Values.length;
var MaxID = 1;
for (var i = 0; i < len; i++)
{
var val = Number(Values[i]);
if (!isNaN(val) && val > MaxID)
MaxID = val;
}
return MaxID;
}
function getRangeByName(spreadSheet, sheetName, rangeName)
{
Logger.log("Trying range: " + "'" + sheetName + "'!" + rangeName);
return spreadSheet.getRangeByName("'" + sheetName + "'!" + rangeName);
}

Copy submitted Form Responses to a New Sheet (PROJECTS) and wanting to be able to edit

I need help please - here's a little history..
I have a google schedule - https://docs.google.com/spreadsheet/ccc?key=0ArmKEmhue9OgdEFwMHBldFpGenNVdzJUQThCQU9Qcmc&usp=sharing
Within that schedule there are numerous sheets but the ones that I was hoping to get help with is the Form Responses 1 and PROJECTS sheet
I have a google form - https://docs.google.com/forms/d/1yFy3i5H3abhFjdvchuJq2ARODcfRGo6KNkOAgeRIMMU/viewform
It's linked to the google schedule and with each submission the data goes in the Form Responses 1 sheet -
There is a script applied to the spreadsheet -
COPYING TO/FROM SCRIPT
/**
* A function named onEdit will be called whenever
* a change is made to the spreadsheet.
*
* #param {object} e The edit event (not used in this case)
*/
function onFormSubmit(e){
var copyFromRange = 'Form Responses 1!A2:AC999';
var copyToRangeStart = 'PROJECTS!A71:AC999';
copyValuesOnly(copyFromRange, copyToRangeStart);
}
/**
* This function will copy the values from a given range to
* a second range, which starts from the given cell reference
*
* #param {string} copyFromRange Range reference eg:
* #param {string} copyToRangeStart Cell reference eg:
*/
function copyValuesOnly(copyFromRange, copyToRangeStart) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var source = ss.getRange(copyFromRange);
source.copyTo(ss.getRange(copyToRangeStart), {contentsOnly: true});
}
This is allowing the data to copy over to the PROJECTS sheet so multiple users can edit and update BUT realized that it overrides the data each time a new submission comes in
Also just to note within the Form Responses 1 sheet under column C "Project Number" there is a forumula =ArrayFormula("MC"&TEXT(ROW(A2:A)-1 ; "000") ) that I placed to allow for automatically numbering each submission with a project number
I also have a ARCHIVE script -
Once a project is final, under column G "Archive" if you type "DONE" it then moves to the ARCHIVE sheet
function onEdit() {
// moves a row from a sheet to another when a magic value is entered in a column
// adjust the following variables to fit your needs
// see https://productforums.google.com/d/topic/docs/ehoCZjFPBao/discussion
var sheetNameToWatch1 = "PROJECTS";
var sheetNameToWatch2 = "ADVERTISING";
var columnNumberToWatch = 7; // column A = 1, B = 2, etc.
var valueToWatch = "DONE";
var sheetNameToMoveTheRowTo = "ARCHIVE";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveCell();
if ( (sheet.getName() == sheetNameToWatch1 || sheet.getName() == sheetNameToWatch2) && range.getColumn() == columnNumberToWatch && range.getValue() == valueToWatch) {
var targetSheet = ss.getSheetByName(sheetNameToMoveTheRowTo);
var targetRange = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
sheet.getRange(range.getRow(), 1, 1, sheet.getLastColumn()).moveTo(targetRange);
sheet.deleteRow(range.getRow());
}
}
ISSUES -COPYING - When a new submission is entered using the form, the data comes in Form Responses 1 sheet and then copies to PROJECTS sheet BUT you cannot edit the data under PROJECTS because each submission overrides any edits and brings in the original form submission data -
What I would like to have happen is the data comes in Form Responses 1 sheet then copies to PROJECTS sheet on each submission but then does not override when a new submission comes in and any changes to the data under PROJECTS stays there
ISSUES - PROJECT NUMBER - the formula setup under Form Responses 1 sheet under column C row 2 - each row has a project number assigned and then it copies to the PROJECTS sheet, when the row is then archived ("DONE) and moves to the ARCHIVE sheet the project number assigned to that row goes with it BUT when you submit a new project the same number can be applied to a new project so then ultimately when you archive all the projects many will have the same project number, is there a way to make each project have a unique project number?
*ISSUES - SUBMIT FORM * - I am not sure why but when a new submission comes in the data is coming in at row 113 each time, not at the end of the form responses rows?
Any help would be greatly appreciated - I thought it was going to work but seems the script isn't the right setup for what I would like it to do
Thanks so much,
Paola G
Here's some pseudo-steps based on how I handle this kind of situation:
onEdit trigger runs when an edit is made to the Spreadsheet
onEdit checks your range for the value that you're looking for. If the value(s) match, proceed to step 3. Otherwise, script does nothing.
Get the row number where the value is changed to the one you're looking for.
Get the Range of that row up to/including whatever column you want to stop at.
Create an Array (or Object) that is a copy of all that data.
"Paste" (add a new row) to the 'completed' Spreadsheet that is the data in that Array (or Object).
Erase the values in the old Spreadsheet.
However, because Form responses are critically tied to the response Spreadsheet, moving rows as you are trying to do is not the best way to go about the problem.
In fact, I would dare to say that you should not be moving at all - instead, what you can do is copy the values from the row you're looking for "DONE" in, and then simply hide that row in the response Spreadsheet. That way, you can think of rows that are not hidden as "still need to be finished.", and projects that are finished will not only be in the response Spreadsheet for "archival/data integrity" purposes, but also in your "completed" Spreadsheet that you can make public-facing, etc. without needing to worry about your Form responses being modified.
Personally I use this strategy for scheduling project reservations for foreign language faculty at my university, and it greatly simplifies the "extra work" required in managing two spreadsheets concurrently. Instead, just hide values you don't need to see and post them to a new Spreadsheet that can act as a record of what's important, all while being able to edit the layout of the Spreadsheet.
PS - If you didn't notice already, you can't break the integrity of the response Spreadsheet, otherwise the Form link would be broken. In fact, Google won't let you make layout-changing edits to response Spreadsheets that are linked to a Form.
I got a script thanks to Riël Notermans (Zzapps)
https://plus.google.com/u/0/116507112418725270540/posts/1i75t1wFEfh
function onFormSubmit(e){
  
 
  var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Form Responses 1");
  var projects =  SpreadsheetApp.getActiveSpreadsheet().getSheetByName("PROJECTS");
  var lastrow = responses.getLastRow();
  var col = responses.getLastColumn();
  var row = responses.getRange(lastrow, 1, 1, col).getValues();
  projects.appendRow(row[0]);
  //copyValuesOnly(copyFromRange, copyToRangeStart);
}
Make sure there are no empty rows beneath both sheets, there are some project numbers. Just make sure the last row is the data.