Google Sheets how to prevent sheet page from being renamed? - google-apps-script

I know I can set an entire page to be protected, but is there a way I can just make the name of the page protected?
I have a Apps Script that depends on the sheet name so I don't want anyone to be able to rename it, but others still need to be able to have edit access to everything else on the sheet.
Thanks!

I don't think that's possible.
But in your script, use the sheet-id instead, which is unique and unchangeable, unless one deletes and recreates the sheet. In which case you could fallback to its name, or just understandably fail. Something like this:
function getSheetById(id) {
var sheets = SpreadsheetApp.getActive().getSheets();
for( var i in sheets )
if( sheets[i].getSheetId() == id )
return sheets[i];
throw new Error('Sheet with id "'+id+'" not found.');
}
Then parametrize and use this getSheetById function instead of using their names. You can see a sheet id by selecting it and looking at your browser's location bar. It's the last '#gid=' part. You can confirm it in code using:
function logId() {
Logger.log(SpreadsheetApp.getActive().getSheetByName('Sheet1').getSheetId());
//click View > Logs
}

Related

Protect sheets after time

I have a spreadsheet with multiple sheets inside. What I want to achieve is for the editors to not be able to edit the sheets after a certain date.
That I can do with creating a script lock function for a sheet but what about the other sheets? Do I create a lock script for each individual sheet? Then how do I program them to run. Basically, I want for 1st script which locks the sheet1 to run today for example, then the next script which locks the sheet2 to run tomorrow same time, the 3rd script which locks sheet3 to run day after tomorrow and so on.
How do I do that, if that's even possible. Or maybe there's an easier way.
Thanks,
You can use the simple trigger onOpen(), this will run this script every-time a user opens the file:
function onOpen() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = ss.getSheets(); //Getting all the sheets from the file.
const lockDates = ss.getSheetByName("LockDates").getDataRange().getValues(); //Getting list of sheets and their lockdates.
const now = new Date(); //Getting today's date.
for (i=0; i < sheets.length; i++){
var currentSheet = sheets[i];
var sheetIndex = (lockDates.flat().indexOf(currentSheet.getName())/2); //This is to get the index where the current sheet name is located.
if(sheetIndex >= 0){ //If the sheet is not on the list we get -1.
var sheetLockDate = lockDates[sheetIndex][1]; //Assiging the lockDate to a variable.
if (now >= sheetLockDate && sheetLockDate > 0){ //Evaluating if today's date is on or after the lockDate.
currentSheet.protect();
console.log('Sheet -' + currentSheet.getName() + '- was protected');
break;
}
else { //The sheet is unprotected if it's still not time to protect it.
currentSheet.protect().remove();
}
}
}
}
Note the following:
This script will determine the lock dates based on a table at "LockDates" sheet, the code might break if you add additional columns.
If the sheet is not included in the list it will not be affected.
If the sheet is included in the list but doesn't have a lockDate it will be unprotected. This will let you modify the lockdate of specific sheets if needed.
You could protect the control sheet "LockDates" and it will not be affected by the script while it is not added to the list.
This is the setup where the code worked:
I think there are 2 ways we can achieve that result:
You can share the file as always but set an access expiration date, you will share access to a file but the access will expire after a specified date https://support.google.com/a/users/answer/9308784.
You can create an Apps Script project, give it a time-driven trigger so a certain function is executed after some period. This function in question should read a list somewhere (perhaps a form or sheet) and remove the access permissions.
#Bryan approach is very similar to mine. Here is my solution:
The code works with a Form with this structure (change the order by modifying the code under the reviewPermissions() function):
And using the Script Editor in the form add the following code:
let deletionSwitch;
function readResponses() {
var responses = FormApp.getActiveForm().getResponses();
responses.forEach(function (response) {
deletionSwitch = false;
reviewPermissions(response);
if (deletionSwitch)
FormApp.getActiveForm().deleteResponse(response.getId());
});
}
function reviewPermissions(response) {
var fileId = response.getItemResponses()[0].getResponse();
var email = response.getItemResponses()[1].getResponse();
var date = response.getItemResponses()[2].getResponse();
var nextPageToken;
if (Date.now() > new Date(date))
do {
var response = getPermissions(fileId, nextPageToken);
var permissions = response.items;
permissions.forEach(function (permission) {
if (permission.emailAddress.toLowerCase() == email.toLowerCase()) {
deletionSwitch = true;
deletePermission(fileId,permission);
}
});
} while (nextPageToken = response.nextPageToken)
}
function getPermissions(fileId, token = null) {
return permissions = Drive.Permissions.list(fileId, {
fields: "nextPageToken,items(id,emailAddress,role)",
pageToken: token
});
}
function deletePermission(fileId,permission){
if (permission.role != "owner")
Drive.Permissions.remove(fileId,permission.id);
}
This code needs Google Drive to be added as an Advanced Google service, add it with the name "Drive". Information about Advanced services is available in this documentation https://developers.google.com/apps-script/guides/services/advanced.
Necessary triggers:
Form onSubmit, execute the readResponses() function.
Time-driven (clock), execute the readResponses() function at the interval you prefer, I recommend every day.
Short code explanation:
The trigger will read all Form entries.
If there is a response that has an older date than today (expired) the code will check all the permissions of the file and will delete all permissions assigned to that email address address in the entry (not case sensitive).
Note:
Entries will be removed once their date expires.
Entries with dates in the future are ignored and checked in future runs.
Permission deletion is retroactive so submitting an entry with a date in the past will cause the permission to be deleted immediately (if exists).
The owner permission can't be removed, the deletion won't be attempted and the entry removed.
This code only works with files you own or have permission editor access to, you can request other people to copy the form with the script and use it with their own files.
Linking the Form responses to a Google Sheet file will allow you to have a historical record of what permissions should expire, this is not necessary for the code to work, just convenient for record purposes. Requesting the email address in the Form should not affect functionality.

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

Writing into another Spreadsheet with SetValue fails without errors/exceptions

I'm trying to update a spreadsheet from a script running on another spreadsheet.
Nothing seems to have any effect on the table (SetValue(), SetBackgroundRGB(), etc.).
I've checked the scope, it includes "https://www.googleapis.com/auth/spreadsheets" permission; besides, this same script has no problem writing to another spreadsheet that it creates in runtime.
function updateAnotheSpreadsheet() {
var targetSpreadsheet = SpreadsheetApp.openById('<target spreadsheet id>');
var sheet = targetSpreadsheet.getSheetByName('<sheet name>');
Browser.msgBox(sheet.getRange("A1").getValue()); // Here I see that my getSheetByName worked
sheet.getRange("A1").setValue('Test value'); // But this does nothing
}
There are no errors but also no effect: nothing changes in the target spreadsheet.
Hi I was testing and was able to do what you are trying to do. You can try to declare a variable for the message, here is what I was able to do, I hope this resolves your inquiry.
function updateAnotherSpreadsheet() {
//Opening the second Spreadsheet by ID
var targetSpreadSheet = SpreadsheetApp.openById("<SpreadSheetId>");
var sheet = targetSpreadSheet.getSheetByName("<SheetName>");
//Getting the range to show on msgBox.
var msg = sheet.getRange("A1:A1").getValue();
//Displaying old data on A1:A1 from the secondary Spreadsheet.
Browser.msgBox("Old data on secondary spreadsheet: " + msg);
//Getting the range and setting new values
sheet.getRange("A1:A1").setValue('Test value').setBackground("teal").setFontColor("white");
}
I would suggest to check for more information here https://developers.google.com/apps-script/guides/sheets.
I hope this helps, greetings.
I found the problem.
A function called from OnEdit() can't ask for permissions. I needed to first update that other spreadsheet from any function called by something "active", like a button; then, after the script asks for permission once, it can do its thing from OnEdit() the next time it runs.

Use App Script to carry out one function, once, on many spreadsheets

I want to run a Google Script which normalises a cell range, on a number (30+) of Google Sheets. Or more precisely, I want a (less technical) user to be able to do this. I can't seem to find a reasonable workflow.
The options I can see are:
Copy/paste the script as a bound script to each of the spreadsheets
That's messy because then there are many copies of the script, which generally won't be run again, and there's a lot of overhead and clicking around to install the macro for each one.
Use a library
I could put the body of the code in a library, then make the copy/paste just a stub (like the accepted answer here ).
However, that's still just as bad for the UX, plus various reports that libraries are messy to deal with.
Make an add-on
The "right way" seems to be to create an add-on which the user can enable for each spreadsheet. However, add-ons still seem to be in "developer preview" mode, and the authorisation cycle is uncertain and potentially slow. Google also expects that The script has been tested with multiple active users. which would be hard - by the time I'd tested it this thoroughly, the job would basically be done. And how would I test it without publishing the add-on anyway?
Other options?
Is there some other way, perhaps using an unbound-script? It's not possible to run a single script once and have it iterate over all the spreadsheets as a bit of user input is required (which range within the spreadsheet etc).
Is there a way where the user could install an unbound script, run it, and it would ask which spreadsheet to run it on?
openByUrl() is really close, but it doesn't actually open the spreadsheet UI, so I wouldn't be able to use functions like getActiveRange() etc.
In case it's relevant, here's the script:
/*function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Normalize')
.addItem('Normalize Crosstab', 'normalizeCrossTab')
.addToUi();
}*/
function onOpen() {
var ss = SpreadsheetApp.getActive();
var items = [
{name: 'Normalize Crosstab', functionName: 'normalizeCrosstab'},
];
ss.addMenu('Normalize', items);
}
/* Converts crosstab format to normalized form. Given columns abcDE, the user puts the cursor somewhere in column D.
The result is a new sheet, NormalizedResult, like this:
a b c Field Value
a1 b1 c1 D D1
a1 b1 c1 E E1
a2 b2 c2 D D2
a2 b2 c2 E E2
...
*/
function normalizeCrosstab() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var firstDataCol = SpreadsheetApp.getActiveRange().getColumn();
var dataCols = values[0].slice(firstDataCol-1);
if (Browser.msgBox("This will create a new sheet, NormalizedResult. Place your cursor is in the first data column.\\n\\n" +
"These will be your data columns: " + dataCols,Browser.Buttons.OK_CANCEL) == "cancel") {
return;
}
var resultssheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NormalizedResult");
if (resultssheet != null) {
SpreadsheetApp.getActive().deleteSheet(resultssheet);
}
var header = values[0].slice(0, firstDataCol - 1);
var newRows = [];
header.push("Field");
header.push("Value");
newRows.push(header);
for (var i = 1; i <= numRows - 1; i++) {
var row = values[i];
for (var datacol = 0; datacol < dataCols.length; datacol ++) {
newRow = row.slice(0, firstDataCol - 1); // copy repeating portion of each row
newRow.push(values[0][firstDataCol - 1 + datacol]); // field name
newRow.push(values[i][firstDataCol - 1 + datacol]); // field value
newRows.push(newRow);
}
}
var newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet("NormalizedResult");
var r = newSheet.getRange(1,1,newRows.length, header.length);
r.setValues(newRows);
};
The first question is: "Who is the owner of all these sheets?" If you are the owner of all these sheets, then you have permission to access them remotely. If you don't own them, then the owner would need to share, and give editing permissions to whatever code is trying to modify their file.
If you own all the spreadsheets, you could create a Stand Alone App do all the processing from a central point. Then you can just email the link of the Stand Alone App to everyone, or have each user enter a link in their spreadsheet to the Stand Alone App. As you mentioned, for that option you won't be able to use methods like, getActiveSheet().
No matter what option you use, you'll need to either have people add something to their spreadsheet, or create some new, centralized interface. The best option for you may come down to ownership and setting permissions.
I'm guessing that if the users of the spreadsheets are the owners, and don't want to give you permission, they'll need to use one of your first three options. And I'd start with the library first.
If you can easily get the file ID's of the spreadsheets, you could create an object that matches the user to the FileID.
var objUserToFileID = {"user1":"abc34ciu89384u", "user2":"FileID_Two", "user3":"FileID_Three"};
Then have a way for the user to choose their name from the list, (Drop Down List) then run the code. That's for the Stand Alone App. Of course, then you'd need to figure out what happens if the user chooses the fileID for someone else's spreadsheet. Then you'd need to have a way to determine who the user of the App is.
You can retrieve the sheets that the user provided the URLs, exhibit them in simple HTML, one sheet below the other, and append a button column, which would call normalizeCrosstab() for that ROW. This is a publishedHTML solution, anybody could use without login.
If there's a defined number of sheets you could also generate them in HTML with a button next to the name, and it would generate the TABLE HTML.
Or use the library, I doubt there's anything you need and couldn't do, that answer is pretty old (12').
I'm using librarys and having no trouble with them, really handy for everything, all sheets must have these 3 functions to work as if the script was in the sheet themself:
function onOpen() {
library.onInitialize();
}
function onEdit(celEd) {
library.onMakeEdit(celEd);
}
function libraryFuncs( funcName, args ){ // Needed for sideBars to use library functions
if(args)
args = args.split("\!|"); // Predefined separation of args
else
args = [];
return library[ funcao ]().apply(this, args);
}

Google Docs: Get Sheet Without Name or ID

I'm writing some scripts for a client with the end goal of complete autonomy -- when complete, the spreadsheet will always work forever. Ideally, anyways.
Because I need information from other sheets, I have to access them in a way other than .getActiveSheet(). Because the client might re-name or re-order the sheets, I have to access the sheet in a way that works even after those changes. This rules out getSheetByName() and getSheets()[SHEET_NUMBER] (again, the client might re-name or re-order the sheets). However, it should be possible because of the "gid." Each sheet has a different gid and they do not change when you re-order or re-name the sheets (scroll to the end of the URL for each sheet to see what I mean).
All of the URL accesses only open the FIRST sheet. For instance,
SpreadsheetApp.openById(SHEET_ID).getDataRange().getValues()
returns the values of the first sheet, even if I include the "gid" part at the end. Same with openById and openFile.
So my question is, how do I access a sheet in a way that will work even after renaming the sheet or reordering the sheets within the spreadsheet?
There's no getSheetById method, but you can build your own using getSheetId(). Here:
function sheetsIdMap() {
var sheetsById = {};
SpreadsheetApp.getActive().getSheets().forEach(function(s){ sheetsById[s.getSheetId()] = s; });
//just checking that it worked
for( var id in sheetsById )
Logger.log(id+' - '+sheetsById[id].getName());
//usage example
var sId2 = sheetsById[2];
Logger.log('\n'+sId2.getName());
}
-- edit
Let's try a more straightforward function (although I don't like to do such a loop and don't store the data on a map for subsequent use o(1)).
function getSheetById(ssID, sheetID) {
var sheets = SpreadsheetApp.openById(ssID).getSheets();
for( var i in sheets )
if( sheets[i].getSheetId() == sheetID )
return sheets[i];
return null; //sheet id not found in spreadsheet, probably deleted?
}
Yes there is a sheet id. Its sheet.getSheetId. this id can be used from apps script and can also be transformed into a "real" gid for making a sheet url. Do (sheetId ^ 31578).toString(36) to get the gid.
I had to reverse-eng it to get it and I cant guarantee it will work forever.