Protecting a cell from consecutive POSTs - google-apps-script

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.

Related

Allow single user to edit Google Spreadsheet at a time

I have Google Spreadsheet say "A" that acts as a repository. Other users can read its content. I want to allow other users to run a script from a different spreadsheet to modify content of "A", but I want it to happen one by one so that changes from all users are recorded & not lost.
I tried doing it by having an additional sheet in "A" with its first cell value having current editor's user name & when current editor releases the sheet it sets value of first cell to blank. So that other users in queue can check if its available for edit. They can start editing it by setting first cell to their username. The problem is a waiting script at waiting editor's end is not able to capture a change in the value of first cell. So even if current editor releases "A", others are not able to start editing.
Please help.
Example:
SS - "A"
Its a data repository which contains data w.r.t. each user.
SS - "B"
This is distributed to all users. They can add their information in it & then press Save so that it gets added to "A". Now if two users click save together its possible that one user's updates are overwritten by second user.
I have this code in SS - "B"
It keeps checking for cell A1 in "Lock" sheet to be empty before proceeding with save command.
function savedata(){
var s1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var wb = SpreadsheetApp.openById("XYZ");
var lck = wb.getSheetByName("Lock");
var uname = getUserEmail();
//Code stucks here, even if cell A1 in "Lock" sheet is set to blank if this loop has started it keeps on looping here. Logger continues to report old value of A1 cell.
while(lck.getRange(1,1).getValue()!=""){
Logger.log(lck.getRange(1,1).getValue(), lck.getRange(1,1).getValue());
}
lck.getRange(1,1).setValue(uname);
//Save data commands
lck.getRange(1,1).setValue("");
};
If all the users edit using the same script from the same sheet, you can use Lock service as mentioned here to prevent concurrent access to the code
Modify the code like so:
function savedata(){
var s1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var wb = SpreadsheetApp.openById("XYZ");
var lck = wb.getSheetByName("Lock");
var uname = getUserEmail();
var lock = LockService.getScriptLock();
var success = lock.tryLock(10000); // Try getting lock for 10sec
if (!success) {
Logger.log("Currently Busy, Try again later")
return
} else {
Logger.log(lck.getRange(1,1).getValue(), lck.getRange(1,1).getValue());
}
lck.getRange(1,1).setValue(uname);
//Save data commands
lck.getRange(1,1).setValue("");
lock.releaseLock() //release lock so someone else can update.
};
This question explains how to use lock service in a concise format, if you would like to know.
Hope that helps!

Delete Row in Google Sheet depending on other Google Sheet cell valuie

Is it possible to write a google script that will delete a row in a Google Sheet, based on cell values for a given range, in another google sheet?
I've done some research on how to do this all in the same google sheet, but my limited experience with writing google sheet scripts has prevented me from understanding if what I described is possible, and how.
This is a script I have so far. This will delete rows in a range of my active spreadsheet if a cell in Column F contains the word "id:123456". What I'd like to be able to do, is to modify this code so that if the word "id:123456" is found, it will look in another column of another Google Sheet, and delete rows that contain "id:123456".
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Sheet1'); // change to your own
var values = s.getDataRange().getValues();
for (var row in values)
if (values[row][6] == 'id:123456')
s.deleteRow(parseInt(row)+1);
};
I haven't tested this code, so you might need to do some modifications to run successfully. But the basic idea is simple: have two sheets open at once, and based on the if from the first sheet make changes in the second sheet.
var ss1 = SpreadsheetApp.getActiveSpreadsheet();
var ss2 = SpreadsheetApp.getByID(0); //Change to the other sheet's ID, or somehow open it.
var s1 = ss1.getSheetByName('Sheet1'); // change to your own
var s2 = ss2.getSheetByName('Sheet1'); // change to your own
var values = s1.getDataRange().getValues();
for (var row in values)
if (values[row][6] == 'id:123456')
var other_values = s2.getDataRange().getValues();
for (var other_row in other_values)
if (other_values[row][6] == 'id:123456')
s2.deleteRow(parseInt(other_row)+1);
Main thing to be worried about and test: I copied your delete code, but since we're deleting rows, unless we start from the bottom we might start missing. (Suppose rows 4 and 8 out of 10 contain something to be deleted; you might end up deleting 4, which turns 8 to 7, but then we still delete row 8--meaning that we've missed a row we should have deleted and deleted a row we shouldn't have.)

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

Replace entire sheet with another in Google Apps Scripts

What I'd like to do is warehouse information from a particular sheet within a spreadsheet and copy it to a second spreadsheet at the end of every day. The second spreadsheet will run complex pivots and reports against the copied information that don't need to run throughout the day.
I can set up a time-driven trigger which will run the job every day within an hour block.
I'm working on the following script which uses SpreadsheetApp.getActiveSpreadsheet to get the current Spreadsheet. Then gets the individual sheet to backup with spreadsheet.getSheetByName. And then uses the sheet.copyTo method to add the current sheet to a new spreadsheet. I'm getting the new spreadsheet by looking up the id with SpreadsheetApp.openById all like this:
function startBackupJob() {
var currentSpreadSheet = SpreadsheetApp.getActiveSpreadsheet()
var masterSheet = currentSpreadSheet.getSheetByName("Sheet1")
var backupSpreadSheetId = "#######################################";
var backupSpreadSheet = SpreadsheetApp.openById(backupSpreadSheetId);
// var backupSheet = backupSpreadSheet.getSheetByName("Sheet1");
// backupSpreadSheet.deleteSheet(backupSheet);
masterSheet.copyTo(backupSpreadSheet).setName("Sheet1");
}
The issue I'm having is that copyTo will create a new worksheet rather than overwrite the existing spreadsheet. The point of moving to the new workbook is to run pivot tables off the data and not re-wire them to point to a new sheet.
I can delete the previous sheet to make room for the new one, but this kills the references on the PivotTable as well, so it doesn't help much.
Is there an easy way to transfer the entire contents of one worksheet to another?
This is similar to (but different from) the following questions:
How do I script making a backup copy of a spreadsheet to an archive folder? - However, I don't want to move the whole file, but a specific sheet within the spreadsheet.
How can copy specifics sheet to another spreadsheet using google script & copy one spreadsheet to another spreadsheet with formatting - However copying produces a new sheet, whereas I need to replace the contents of an existing sheet
Scripts, copy cell from one sheet to another sheet EVERYDAY at a specific time - However, I do want to replace the entire sheet, rather than just specific cells within the sheet.
Update
I might be able to do this by calling getRange on each sheet and then using getValues and setValues like this:
var currentValues = masterSheet.getRange(1, 1, 50, 50).getValues()
backupSheet.getRange(1, 1, 50, 50).setValues(currentValues)
But I'm worried about edge cases where the master sheet has a different available range than the backup sheet. I also don't want to hardcode in the range, but for it to encompass the entire sheet. If I call .getRange("A:E") then the two worksheets have to have the exact same number of rows which is not likely.
Your update has you about 90% of the way there. The trick is to explicitly check the size of the destination sheet before you copy data into it. For example, if I did something like this:
var cromulentDocument = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = cromulentDocument.getSheetByName('master');
var logSheet = cromulentDocument.getSheetByName('log');
var hugeData = masterSheet.getDataRange().getValues();
var rowsInHugeData = hugeData.length;
var colsInHugeData = hugeData[0].length;
/* cross fingers */
logSheet.getRange(1, 1, rowsInHugeData, colsInHugeData).setValues(hugeData);
...then my success would totally depend upon whether logSheet was at least as big as masterSheet. That's obvious, but what's less so is that if logSheet is bigger then there will be some old junk left over around the edges. Ungood.
Let's try something else. As before, we'll grab the master data, but we'll also resize logSheet. If we don't care about logSheet being too big we could probably just clear() the data in it, but let's be tidy.
var cromulentDocument = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = cromulentDocument.getSheetByName('master');
var logSheet = cromulentDocument.getSheetByName('log');
var hugeData = masterSheet.getDataRange().getValues();
var rowsInHugeData = hugeData.length;
var colsInHugeData = hugeData[0].length;
/* no finger crossing necessary */
var rowsInLogSheet = logSheet.getMaxRows();
var colsInLogSheet = logSheet.getMaxColumns();
/* adjust logSheet length, but only if we need to... */
if (rowsInLogSheet < rowsInHugeData) {
logSheet.insertRowsAfter(rowsInLogSheet, rowsInHugeData - rowsInLogSheet);
} else if (rowsInLogSheet > rowsInHugeData) {
logSheet.deleteRows(rowsInHugeData, rowsInLogSheet - rowsInHugeData);
}
/* likewise, adjust width */
if (colsInLogSheet < colsInHugeData) {
logSheet.insertColumnsAfter(colsInLogSheet, colsInHugeData - colsInLogSheet);
} else if (colsInLogSheet > colsInHugeData) {
logSheet.deleteColumns(colsInHugeData, colsInLogSheet - colsInHugeData);
}
/* barring typos, insert data with confidence */
logSheet.getRange(1, 1, rowsInHugeData, colsInHugeData).setValues(hugeData);
What's going on here is pretty straightforward. We figure out how big the log needs to be, and then adjust the destination sheet's size to match that data.

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