Protect sheets after time - google-apps-script

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.

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

Hiding google sheet tab from users [duplicate]

This question already has an answer here:
Google Spreadsheet - Show sheets depending on type of user
(1 answer)
Closed 2 years ago.
I have a google sheets document with two tabs one called called internal and the other called external. How can i hide the internal tab from other users? the lock function already avialble is not good enough I only want people from my company to be able to see both tabs, clients should only be able to see the external tab.
function validUsers() {
String[] adminUsers = {”email1#gmail.com”,”email2#gmail.com”,”email3#gmail.com”};
if (adminUsers.indexOf(Session.getEffectiveUser().getEmail()) >= 0) {
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Internal').showSheet()
else
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Internal').hideSheet()
}
}
Issue:
You want to hide or show a sheet in your spreadsheet depending on which user is accessing the spreadsheet.
Solution:
You could do the following:
Install an onOpen trigger which executes a function (let's call it fireOnOpen) every time a user opens the spreadsheet.
The function fireOnOpen should check which user is accessing the spreadsheet, and hide or show a certain sheet (called Internal) depending on this.
In order to check the current user accessing the spreadsheet, you can use getActiveUser() (instead of getEffectiveUser(), which will return the user who installed the trigger).
Workflow:
The trigger can be installed either manually or programmatically. To do it programmatically, copy this function to your script editor and execute it once:
function createOnOpenTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("fireOnOpen")
.forSpreadsheet(ss)
.onOpen()
.create();
}
This will result in fireOnOpen being executed every time a user accessed the spreadsheet. The fireOnOpen function could be something like this:
function fireOnOpen() {
const adminUsers = ["email1#gmail.com","email2#gmail.com","email3#gmail.com"];
const currentUser = Session.getActiveUser().getEmail();
const internalSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Internal");
if (adminUsers.includes(currentUser)) internalSheet.showSheet();
else internalSheet.hideSheet();
}
Important notes:
You cannot hide sheets for some users but not for others. A hidden sheet is hidden for all users, and a visible sheet is visible for all users. Therefore, this will only work if internal and external users don't access the spreadsheet at the same time. If they do, external users might be able to access the Internal sheet.
getActiveUser() is not always populated, as you can see on this answer, so please make sure that all admin users are from the same G Suite domain. Otherwise, this won't work.
If the privacy of the Internal sheet is critical and there is a possibility of internal and external users accessing the spreadsheet at the time, I would not recommend this solution.
Edit:
As mentioned in comments, a possible workaround for the occasions when admin and non-admin users access the file at the time could be the following:
When an admin user accesses the file, store the time in which that happened.
Create a time-driven trigger to execute a function periodically (every 5 minutes, let's say), which will check if an admin accessed the file a short time ago (let's say 30 minutes). If the admin has done that, remove the Permissions for the different non-admin domains. If that's not the case, add these Permissions back.
Enabling the Drive Advanced Service would be required in this case.
Updated code sample:
function fireOnOpen() {
const adminUsers = ["email1#gmail.com","email2#gmail.com","email3#gmail.com"];
const currentUser = Session.getActiveUser().getEmail();
const internalSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Internal");
if (adminUsers.includes(currentUser)) {
internalSheet.showSheet();
const documentProperties = PropertiesService.getDocumentProperties();
documentProperties.setProperty("lastAdminAccess", new Date().getTime()); // Store time of admin access
} else internalSheet.hideSheet();
}
function createOnOpenTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("fireOnOpen")
.forSpreadsheet(ss)
.onOpen()
.create();
}
function updatePermissions() {
const fileId = SpreadsheetApp.getActive().getId();
const lastAdminAccess = PropertiesService.getDocumentProperties().getProperty("lastAdminAccess"); // Last time of admin access in ms
const now = new Date().getTime(); // Current time in milliseconds
const thirtyMinutes = 1000 * 60 * 30; // 30 minutes in milliseconds
if (now - lastAdminAccess < thirtyMinutes) {
const currentPermissions = Drive.Permissions.list(fileId)["items"];
const publicPermissionIds = currentPermissions.filter(permission => permission["type"] === "anyone")
.map(permission => permission["id"]);
publicPermissionIds.forEach(permissionId => Drive.Permissions.remove(fileId, permissionId));
} else {
const resource = {
type: "anyone",
role: "reader"
}
Drive.Permissions.insert(resource, fileId);
}
}
function createTimeTrigger() {
ScriptApp.newTrigger("updatePermissions")
.timeBased()
.everyMinutes(5)
.create();
}
As soon as you share a sheet you should assume that anyone can see the data in it. Even if someone shouldn't be able to see the internal tab, they can always e.g. make a copy of the sheet and thus get to the data.
You could try creating a separate sheet and using =IMPORTRANGE() to refer to the original one. But know that once you allow the connection between the two sheets, anyone with access to the second one might be able to access anything in the first one. Maybe get around that using three sheets:
Internal + External - your current sheet
A sheet-in-the-middle that only you can access. It has a single tab Internal that uses =IMPORTRANGE() to access data from 1)
The External sheet for clients. Linked to 2) through =IMPORTRANGE()
This way 3) only has access to the data in 2) which in turn only includes a link to 1).
I do not promise that this will make the data safe from those who shouldn't see it. But it will at least be safer.

Google Sheets: Action Based on Birthday

I'm trying to send myself either an email or copy the row to a new sheet when it's someone's birthday or hire date anniversary. Copying the line to a new sheet would allow me to use zapier to notify me of the update. Either would work. The sheet uses a form to collect data.
I've built a few scripts but nothing that had to do with dates. I'm just struggling with this one and have tried a few examples I could find with no luck.
Here is this sheet. It's view only so just let me know if you need more access.
I understand that you want to replicate your form responses Sheet in another Sheet (let's call it Zapier Sheet) automatically each time that a new form response is added. You can achieve that goal developing an Apps Script code that runs at each form response. In that case you can use a code similar to this one:
function so62400514() {
var formSheet = SpreadsheetApp.openById(
'{FORM SHEET ID}').getSheets()[0];
var zapierSheet = SpreadsheetApp.openById(
'{ZAPIER SHEET ID}').getSheets()[0];
var formData = formSheet.getRange(1, 1, formSheet.getLastRow(), formSheet
.getLastColumn()).getValues();
var zapierData = zapierSheet.getRange(1, 1, zapierSheet.getLastRow(),
formSheet.getLastColumn()).getValues();
var recorded = false;
for (var fr = 0; fr < formData.length; fr++) {
for (var zr = 0; zr < zapierData.length; zr++) {
if (formData[fr].toLocaleString() == zapierData[zr].toLocaleString()) {
recorded = true;
}
}
if (recorded == false) {
zapierSheet.appendRow(formData[fr]);
} else {
recorded = false;
}
}
}
This code will first open both sheets (using SpreadsheetApp.openById() and Spreadsheet.getSheets()) to select the data with Sheet.getRange (setting boundaries with Sheet.getLastRow() and Sheet.getLastColumn()) and reading it using Range.getValues(). After that operation the data will get iterated using the property Array.length as the perimeter. The iteration compares each row from the form Sheet to every row of the zapier sheet (to accomplish that, I first parsed the row as a string with Date.toLocaleString()). If the form row is found in the zapier sheet, the boolean recorded will flag to true. After every row on the zapier sheet gets compared to the form row, the code will write it down on the zapier sheet based on the boolean flag.
As explained in the previous paragraph, this code will take the form sheet rows not present in the zapier sheet; and paste them on the zapier sheet. I used this approach to prevent missing any row (as it could happen when simultaneous users answer the form all at once). To make this fire automatically you'll need to set up an installable trigger with these settings:
As an example, let's say that we have these form responses:
And our initial sample zapier sheet looks like this one below. Please, notice how several past rows are missing;
After running the script (as it will do automatically) this would be the result:
I suggest running the script manually for an initial setup. If the timestamps diverge, please check if both spreadsheets share time zones. Don't hesitate to ask me further questions to clarify my answer.

How to trigger Google Apps script function based on insert row via api

I have a Google Sheet with 5 columns (First Name, Address, SKU, Quote, Status).
I have an apps script function (createQuote) which looks at the above variable's values from google sheet row and create a google document quote replacing the variables to values.
I use Zapier to insert row into my above google sheet.
What am struggling with-:
I need a way to trigger my createQuote function right when a new row is inserted via zapier (Google Sheet API call).
I tried playing with triggers but couldn't make it, any help is appreciated.
thank you
here is the code for my function-
function quoteCreator(){
docTemplate = "googledocidgoeshere"
docName = "Proposal"
var sheet = SpreadsheetApp.getActive().getSheetByName("Main")
var values = sheet.getDataRange().getValues()
var full_name = values[1][0]
var copyId = DriveApp.getFileById(docTemplate).makeCopy(docName+" for "+full_name).getId()
// Open the temporary document
var copyDoc = DocumentApp.openById(copyId);
// Get the document’s body section
var copyBody = copyDoc.getActiveSection();
// Replace place holder keys/tags,
copyBody.replaceText("keyFullName", full_name);
copyDoc.saveAndClose();
// Convert temporary document to PDF by using the getAs blob conversion
var pdf = DriveApp.getFileById(copyId).getAs("application/pdf");
// put the link of created quote in the quote column
var url = DocumentApp.openById(copyId).getUrl()
var last = sheet.getRange(2, 7, 1, 1).setValue(url)
}
Note-: I haven't put the loop yet in above, i'll do that once it starts working as per my requirements.
Changes made via Sheets API or Apps Script do not fire onEdit triggers. I give two workarounds for this.
Web app
Have whatever process updates the sheet also send a GET or POST request to your script, deployed as a web application. As an example, a GET version might access https://script.google.com/.../exec?run=quoteCreator
function doGet(e) {
if (e.parameter.run == "quoteCreator") {
quoteCreator();
return ContentService.createTextOutput("Quote updated");
}
else {
return ContentService.createTextOutput("Unrecognized command");
}
}
The web application should be published in a way that makes it possible for your other process to do the above; usually this means "everyone, even anonymous". If security is an issue, adding a token parameter may help, e.g., the URL would have &token=myToken where myToken is a string that the webapp will check using e.parameter.token.
GET method is used for illustration here, you may find that POST makes more sense for this operation.
Important: when execution is triggered by a GET or POST request, the methods getActive... are not available. You'll need to open any spreadsheets you need using their Id or URL (see openById, openByUrl).
Timed trigger
Have a function running on time intervals (say, every 5 minutes) that checks the number of rows in the sheet and fires quoteCreator if needed. The function checkNewRows stores the number of nonempty rows in Script Properties, so changes can be detected.
function checkNewRows() {
var sp = PropertiesService.getScriptProperties();
var oldRows = sp.getProperty("rows") || 0;
var newRows = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Main").getLastRow();
if (newRows > oldRows) {
sp.setProperty("rows", newRows);
quoteCreator();
}
}

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