Allow single user to edit Google Spreadsheet at a time - google-apps-script

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!

Related

Installable trigger needs to grab email of user who edited, but is only accessing mine

So I've got a spreadsheet that lists work to be done via an imported range from another file. Users have a dropdown of validated data that when they select the status of this work it searches the other sheet for the work they've selected and updates the status of this work. It's super simple, and works for everyone I've given editing privileges to.
I'd like alter it to also log the email of the user who edited the work status and therefore ran the script (we are all part of the same workspace domain). I've gotten it to pull my email and place it where required with repetition, but I cannot get it to access anyone else's. I tried deploying it, although I'm not entirely sure I understand how that works. I've looked into authorizing it, but the only place I can find to alter authorization is via the appscript.json in the editor, but that isn't showing the permissions that the documentation says to edit/add so I'm a little lost as to how to authorize this.
Not sure if it matters, but this script is attached to the sheet it picks up the edit from. I don't know if that means the sheet permissions need to change or what.
Here is the entirety of the code, minus identifying URL's/ID's:
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); //shortens calling to the current sheet
var dataSheet = SpreadsheetApp.openById("datafileID").getSheetByName("Data"); //shortens calling to the data file
const status = e.value; //validated user input
var erange = e.range; //range of edited cell
var ecolumn = erange.getColumn(); //range column of edited cell
var erow = erange.getRow(); //range row of edited cell
var snRow = erow; //identifies what row to look for the store number
var snColumn = ecolumn-2; //identifies what column to look for the store number
var sn = sheet.getRange(snRow, snColumn).getValue(); //declares the store number as variable
var user = e.user; //declares user as variable
if (!e.range.isBlank()) { //searches data sheet for store and updates status and user
var column = dataSheet.getRange("G:G").getValues();
var uRow;
for (var i = 0; i < column.length; i++){
if (column[i][0] === sn) {
uRow = i+1;
break;
}
}
dataSheet.getRange(uRow,6).setValue(status)
dataSheet.getRange(uRow,5).setValue(user)
}
sheet.getActiveCell().clearContent();
}
onEdit is a reserved name for simple triggers, you should not use call this function from an edit / change installable trigger because there will be two executions running in parallel.
When using an simple or installable trigger with Google Workspace accounts from the same domain, e.user should return the User object representing de active user.
As the script is working for your account there is no need of additional permissions.
As the script is not working, try the following:
Delete the installable trigger
Change the name of the function (i.e. respondToEdit)
Create the installable trigger again pointing to the new function name.
Double check that the spreadsheet sharing permissions are set to editors from your Google Workspace domain only.
References
https://developers.google.com/apps-script/guides/triggers/events#edit

Selected cell does not update when using button linked to script

I have a sheet for users to enter a cash-count and their initials then hit a button which runs a script and stores these inputs in a table.
The problem is, if the user types the information (e.g. their initials) and hits the button without first pressing Return or selecting another cell, the information is not saved.
I've seen a similar post:
How to force flush a user's input?
and tried the solutions, some don't work and none are really what I'm looking for. I know that I could use a Checkbox instead of a button but I'd like to find a way do it without resorting to that. The table is a cash-count for a till so not all cells require a value (there may be no $100 notes for example) and I won't know which was the last cell edited.
[The Data][1]
[1]: https://i.stack.imgur.com/mdKXk.png
The Code:
function Accept() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var db = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DATA");
var vals = db.getRange("B:B").getValues();
var last = vals.filter(String).length;
var ui = SpreadsheetApp.getUi();
var user = ui.prompt("Notes:");
var values = [[
sheet.getRange("I7").getValue(), //Date/Time
sheet.getRange("N16").getValue(), //Amount
sheet.getRange("E17").getValue(), //Initials
user.getResponseText(), //Notes
]];
db.getRange(last+1,2,1,4).setValues(values);
}
One solution is to utilize a checkbox as the Accept button, and use an onEdit(e) simple trigger to run your Accept() function.
See the checkboxButtons_ script for sample code.

Creating a backup of data entered into a sheet via Google App Scripts

I have a spreadsheet where users can enter data and then execute a function when clicking on a button. When the button is clicked it logs the time and entered data in a new row on another sheet in that spreadsheet.
To make sure that sheet is not accidentally edited by the users I want to create a non-shared backup of that data.
I import the range to another spreadsheet, but just importing the range means that if the original sheet is edited/erased that data will also be edited/erased, so I wrote the following script to log the changes as they come in.
function onEdit(event){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var incomingSheet = ss.getSheetByName('Incoming');
var lastRow = incomingSheet.getLastRow();
var incomingData = incomingSheet.getRange(lastRow,1,1,7);
var permanentSheet = ss.getSheetByName('PermanentLog')
var newdataRow = permanentSheet.getLastRow();
incomingData.copyTo(permanentSheet.getRange(newdataRow+1,1));
}
This works when Run from the Apps Script Editor, however, when I enter new data and click the button on the original spreadsheet, it logs the data to the log sheet there, and the range is imported to the 'Incoming' sheet of the new Spreadsheet, but the data is not copied over to the 'Permanent Log' sheet (unless I Run it manually from within the Apps Script Editor). It also works if I remove the ImportRange function from the first sheet and then just manually enter data in on the 'Incoming' sheet.
So does this mean new rows from an Imported Range do not trigger onEdit? What would be the solution? I don't want to run this on a timed trigger, I want to permanently capture each new row of data as it comes in.
Also, am I overlooking a more elegant and simple solution to this whole problem?
Thank you for your time.
This function will copy the data to a new Spreadsheet whenever you edit column 7 which I assume is the last column in your data. It only does it for the sheets that you specify in the names array. Note: you cannot run this from the script editor without getting an error unless you provide the event object which replaces the e. I used an installable onEdit trigger.
The function also appends a timestamp and a row number to the beginning of the archive data row
function onMyEdit(e) {
e.source.toast('entry');//just a toast showing that the function is working for debug purposes
const sh = e.range.getSheet();//active sheet name
const names = ['Sheet1', 'Sheet2'];//sheetname this function operates in
if (~names.indexOf(sh.getName()) && e.range.columnStart == 7) {
const ass = SpreadsheetApp.openById('ssid');//archive spreadsheet
const ash = ass.getSheetByName('PermanentLog');//archive sheet
let row = sh.getRange(e.range.rowStart, 1, 1, 7).getValues()[0];
let ts = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy/MM/dd HH:mm:ss");//timestamp
row.unshift(ts, e.range.rowStart);//add timestamp and row number to beginning
Logger.log(row);//logs the row for debug purposes
ash.appendRow(row);//appends row to bottom of data with ts and row
}
Logger.log(JSON.stringify(e));
}
Restrictions
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.
https://developers.google.com/apps-script/guides/triggers
So yeah, as far as I understand you it can't be done that way.

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.

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