I have made a google script function to update a specific page for multiple different spreadsheets with identical pages. So the function takes the name and position of the sheet to be changed (from a master spreadsheet) and then loops through each of the other spreadsheets and replaces the old specified sheet with the new specified one. My problem is some of the sheets need to be protected but when it replaces them it removes protection.
I've tried to use sheet.protect() but I get "TypeError: Cannot find function protect in object Spreadsheet". I am unaware why since in the google docs I use similar code. I also tried using the getProtections() but it couldn't find the function either.
function updatePage(pageName, pagePos) {
var source = SpreadsheetApp.getActiveSpreadsheet();
var sheetToUp = source.getSheetByName(pageName);
for (i=0; i < schools.length; i++) {
//schools[i][1] provides the link to the spreadsheet
if(schools[i][1] != 0) {
sheet = SpreadsheetApp.openById(schools[i][1]);
sheetToUp.copyTo(sheet);
sheet.deleteSheet(sheet.getSheetByName(pageName));
sheet.setActiveSheet(sheet.getSheetByName("Copy of " + pageName));
sheet.renameActiveSheet(pageName);
sheet.setActiveSheet(sheet.getSheetByName(pageName));
sheet.moveActiveSheet(pagePos);
var protection = sheet.protect(); //problem
}
}
}
It should output a new protected sheet with name pageName at position pagePos in the spreadsheet linked in school[i][1]. It outputs this but its unprotected.
Related
onEdit and onChanged does not seem to trigger when removing or giving permission to a user.
I have not seen any event related to that in the documentation.
If there is no such events, is there a way to programatically track permission changes?
Edit: I am sorry if the question was not clear but I meant sheet protection permissions for individual sheets inside a workbook instead of Drive sharing permission for the workbook.
If I understand you correctly, you want to keep track of changes in Sheet protections. As you said, there are no Apps Script triggers that can track this.
Workaround (time-based trigger and Properties Service):
As a workaround, I'd propose doing the following:
Create a time-based trigger that will fire a function periodically (every 1 minute, for example). You can do this either manually, or programmatically, by running this function once:
function createTrigger() {
ScriptApp.newTrigger("trackProtections")
.timeBased()
.everyMinutes(1)
.create();
}
This will fire the function trackProtections every minute. This function's purpose is to track changes to the sheet protections since last time it was fired (in this example, 1 minute ago).
In the triggered function, retrieve the current sheet protections, store them in script properties, and compare the previously store protections to the current ones (check editors, etc.). You could use JSON.stringify() and JSON.parse() to be able to store these protections to script properties (which only accept strings) and convert them back. It could be something along the following lines (check inline comments):
function trackProtections() {
var scriptProperties = PropertiesService.getScriptProperties(); // Get script properties (old protections)
var ss = SpreadsheetApp.getActive();
var sheets = ss.getSheets(); // Get all sheets in spreadsheet
sheets.forEach(function(sheet) { // Iterate through each sheet in the spreadsheet
var sheetName = sheet.getName();
// Get sheet current protection (null if it's unprotected):
var protection = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
// Get previous editors of current sheet (stored in script properties):
var oldEditors;
if (scriptProperties.getProperty(sheetName) !== null) {
oldEditors = scriptProperties.getProperty(sheetName).split(',');
}
// Get current editors of current sheet:
var newEditors;
if (protection) {
newEditors = protection.getEditors().map(function(editor) {
return editor.getEmail();
});
}
if (oldEditors && !newEditors) { // Protection for the sheet was removed
scriptProperties.deleteProperty(sheetName); // Remove old property (protection doesn't exist anymore)
Logger.log('protection for ' + sheetName + ' was removed!');
} else if (!oldEditors && !newEditors) { // There were and there aren't any protections for the sheet
Logger.log('there are no protections');
} else if (!oldEditors && newEditors) { // Protection for the sheet was added
Logger.log('protection for ' + sheetName + ' was added!');
scriptProperties.setProperty(sheetName, newEditors.toString()); // Add script property with current editors
} else {
if (newEditors.sort().join(',') !== oldEditors.sort().join(',')) { // Check if old and current editors are the same
var addedEditors = newEditors.filter(function(editor) {
return oldEditors.indexOf(editor) === -1; // Return editors that are in current protection but not in old one (added)
});
var removedEditors = oldEditors.filter(function(editor) {
return newEditors.indexOf(editor) === -1; // Return editors that were in old protection but not in current one (removed)
});
Logger.log('protection for ' + sheetName + ' was modified!');
Logger.log('these editors were added: ' + JSON.stringify(addedEditors));
Logger.log('these editors were removed: ' + JSON.stringify(removedEditors));
scriptProperties.setProperty(sheetName, newEditors.toString()); // Add script property with current editors
}
}
});
}
This function iterates through all sheets in the spreadsheet, and looks for changes in the protection since last execution (whether the sheet became protected, or unprotected, or whether the editors changed).
In this sample, the protections are getting stored in script properties the following way: each property has the name of the sheet as the key and the emails of the different editors of the sheet as value (comma-separated). The script is using toString and split to store the array of editor emails in the property, because script properties only accept strings as value.
You could also keep track of other protection settings that change (description, etc.) which would make the script a bit more complex (and you'd probably have to use JSON.stringify and JSON.parse, but I hope this simplified example will be enough for you to understand the workflow.
The function should also check if there is a change between the current protections and the ones that were previously stored, and do whatever you had planned to do when sheet protections changed. Since I don't know what you wanted to do in this situation, I'm just using Logger.log to log the changes that happened to the protection since last execution.
Reference:
Class Protection
Class ClockTriggerBuilder
Class PropertiesService
I have a Google Add-on that manages a few sheets in a Google spreadsheet. One of the things it does is remove sheets based on dates, which are the names of the sheets it deletes. In my current development phase I'm also adding the capability to remove sheets that include the date and another term, specifically "script 6/5/2019", for example.
I'm using the same code that worked for the sheets named with the date and made some adjustments to it, but it returns an error when it comes time to delete the sheet: "Cannot find method deleteSheet(string)"
if(sRemove==true) {
SpreadsheetApp.getActiveSpreadsheet().toast('Finding Old Scripts', 'Status',3);
for (var i = ss.getNumSheets()-1; i >= 0; i--) {
var thisTab = ss.getSheets()[i].getName();
if(thisTab.substr(0,6) =="script") {
Logger.log(i+" "+thisTab+" is a script");
var tabDate = new Date(thisTab.substr(8));
//8 is the first digit of the date
Logger.log(tabDate);
var thisDate = new Date(todayDate);
var patt = new RegExp("s");
var res = patt.test(thisTab);
Logger.log(i+" "+res);
if(tabDate<thisDate && res==true) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
//ss.setActiveSheet(ss.getSheetByName(thisTab));
//ss.deleteActiveSheet(ss.getSheetByName(thisTab));
Logger.log(thisTab);
ss.deleteSheet(thisTab);
tabsGone++;
}
}
}
ui.alert(tabsGone+" Sheets Removed");
}
Logger.log returns the correct name of the sheet to be removed, but deleting the sheet returns the error "Cannot find method deleteSheet(string)"
Requirement:
Delete sheet based on name.
Solution:
Use getSheetByName() to get the sheet object then pass this to your deleteSheet():
ss.deleteSheet(ss.getSheetByName(thisTab));
Explanation:
Currently you're passing string thisTab to deleteSheet(), this won't work because it is expecting a sheet object, not a string. As long as your code above is working properly and thisTab matches your sheet name exactly, all you need to do is call getSheetByName(thisTab) to get the sheet object then pass this to deleteSheet().
References:
getSheetByName(string)
deleteSheet(sheet)
I am having issues getting information from a Google Form into a Google Sheet. I am looking to get the edit url onFormSubmit and then set it to the end of the record in a column where the responses are stored.
Research:
I asked this question, which started as a script bound to the sheet but trying to access the form. It then became a script bound to the form, trying to access the sheet.
I found this question which looks to be related to my question (with a slightly different use case). Similarly to mine, I think it will have issues getting spreadsheet methods while on the form.
Since both required methods that are only available to either the script or the form I keep hitting a wall. Now I am thinking that I may need a hybrid solution that requires some of the code to be bound to the sheet, and some to be bound to the form, with a variable passed between the two scripts that are both executing onFormSubmit.
This is what I think I should keep bound to the form
function onFormSubmit(e)
{
Logger.clear; //if I can use log to pass variable I want to clear out at the beginning of each submission
var form = FormApp.getActiveForm();
var activeFormUrl = form.getEditUrl();//This is the variable I need to pass to the sheet
Logger.log(activeFormUrl); //only to confirm what we are getting unless I can somehow access the log after the fact using sheet script
}//This is the end of onFormSubmit function bound to the Form
This is what I think I should keep bound to the sheet
function onFormSubmit(e)
{
var ss = SpreadsheetApp.getActiveSheet();
var createDateColumn = ss.getMaxColumns(); //CreateDateColumn is currently in AX (Column 50) which is the last/max column position
var urlColumn = createDateColumn-1; //urlColumn is currently in AX (Column 50) Calculating using it's relative position to createDateColumn Position
if (ss.getActiveRange(urlColumn).getValue() == "") // so that subsequent edits to Google Form don't overwrite editResponseURL
{
var editResponseURL = setGoogleFormEditUrl(ss, createDateColumn, activeFormUrl);
var createEditResponseUrl = ss.getActiveRange(urlColumn);
createEditResponseUrl.setValue(activeFormUrl);
}
else
{
if (ss.getActiveRange(urlColumn).getValue() != activeFormUrl)
{
Logger.log("Something went wrong - URL doesn't match" + activeFormUrl);
Logger.log(ss.getActiveRange(urlColumn).getValue());
var checkLog2 = Logger.getLog();
}
else {}//do nothing
}
}//This is the end of the onFormSubmit function bound to the Sheet
What I need to know is how to take activeFormUrl from the form script and send it to the sheet script. Can I use the log?
I'm not sure if this would work for you, but you can make an HTTPS GET or POST request to an Apps Script project with UrlFetchApp.fetch(url). So, from the Form project, you can make an HTTPS POST request to a published Web App. The published Web App can actually be published from the project bound to the spreadsheet, if you want to do that.
The way that an Apps Script project detects an HTTPS GET or POST request being sent to it, is with either a doGet() or doPost() function.
var webAppUrl = "https://script.google.com/macros/s/123_My_FileID/exec";
var payload = {
"url":"activeFormUrl"
};
var options = {"method":"post","payload":payload};
UrlFetchApp.fetch(webAppUrl, options);
The above code makes a POST request to another Apps Script project, and sends the payload to the file.
function doPost(e) {
var theUrl = e.parameter.url;
};
I'm assuming that you are trying to have a spreadsheet that is getting data from multiple Forms?
I had to separate the form and the spreadsheet operations as getting the formEditURL using the FormApp method would not work if I was using other SpreadsheetApp methods in the same function and the FormApp method only worked if it was in the onFormSubmit function.
Here is the code snippet which I used successfully
function onFormSubmit(e)
{
var rng = e.range; //Collects active range for event
var ss = SpreadsheetApp.getActiveSpreadsheet();//collects active spreadsheet object
var fUrl = ss.getFormUrl();//gets form url linked with active spreadsheet
var f = FormApp.openByUrl(fUrl);//opens form using form url
var rs = f.getResponses(); //gets responses from active form
var r = rs[rs.length - 1]; // Get last response made on active form
var c = getCellRngByCol(rng, 'formEditURL'); //locates the cell which the form url will be stored by searching header name
c.setValue(r.getEditResponseUrl());// sets form url value into correct cell for active form response
var callSpreadsheetFunctions = spreadsheetFunctions(rng, ss); //method calls other spreadsheet functions. This had to be modularized as you can't get form url if the other functions are occuring in the same function
}//This is the end of the onFormSubmit function
function spreadsheetFunctions (rng, ss)
{
var rowIndex = rng.getRowIndex();//gets row index for current response. This is used by tracking number
var createDateCell = getCellRngByCol(rng, 'CreateDate'); //locates which cell the createdate will be stored in by searching header name
var timestampCell = getCellRngByCol(rng, 'Timestamp'); //locates which cell the autogenerated timestamp is located in by searching header name
var trackingNumberCell = getCellRngByCol(rng, 'Tracking ID#');//locates which cell the tracking ID# will be stored in by searching by header name
var createDate = setCreateDate(rng, createDateCell, timestampCell); //method sets create date. NOTE: Function not included in code snippet but left here to demonstrate type of information used
var trackingNumber = setTrackingNumber(rng, rowIndex, trackingNumberCell, createDateCell); //method sets tracking number. NOTE: Function not included in code snippet but left here to demonstrate type of information used
return;
} //This is the end of the callSpreadsheetFunctions function
function getCellRngByCol(rng, col)//finds the cell associated with the active range and column
{
var aRng = SpreadsheetApp.getActiveSheet().getDataRange();//gets the spreadsheet data range
var hRng = aRng.offset(0, 0, 1, aRng.getNumColumns()).getValues();//finds the header row range by offsetting
var colIndex = hRng[0].indexOf(col);// declares the column index in the header row
return SpreadsheetApp.getActiveSheet().getRange(rng.getRow(), colIndex + 1); //returns the cell range at the position of the active row and column name passed into this method
}//This is the end of the getCellRngByCol function
I'm trying to permanently lock/protect certain cells on 14 different sheets (1 hidden from the workers for formula stuff). I have them all locked and no one can edit if I add them to it as an editor. But it is the template, I make copies of it for each client (and new clients) for the staff. The staff that works on the sheet and the employees are only allowed to edit certain cells for the work they do.
The problem is if I have Workbook1 with X cells locked on the different sheets, make a copy, rename it to Workbook - Client#ID, then add them employees John and Jane, who will be working on this client, as editors; they can now edit every cell, including the protected ones (they get added as editors to the protected cells too). It doesn't do this on the original, it only happens to the copy made of the template. I then have to go through all 13 sheets and remove them from the protected cells.
I'm trying to quickly remove them automatically with a script add-on that I want to turn into a button or something later...
Or is there a better way to fix this bug?
Google has an example of removing users and keeping sheet protected and I have tried to add in what I need to make it work, but it doesn't do anything when I run the test as an add-on for the spreadsheet. I open a new app script project from my spreadsheet and enter in the example code from google
// Protect the active sheet, then remove all other users from the list of editors.
var sheet = SpreadsheetApp.setActiveSheet(January);
var protection = sheet.protect().setDescription('Activity Log');
var unprotected = sheet.getRange('A2:N7');
protection.setUnprotectedRanges([unprotected]);
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
For this you can write a script function to set the protection ranges and add editors for the sheets as well.
Please check the sample apps script code to add protection for a range in a sheet below:
function addProtection()
{
// Protect range A1:B10, then remove all other users from the list of editors.
var ss = SpreadsheetApp.getActive();
var range = ss.getRange('A1:B10');
var protection = range.protect().setDescription('Sample protected range');
// var me = Session.getEffectiveUser();
// array of emails to add them as editors of the range
protection.addEditors(['email1','email2']);
// array of emails to remove the users from list of editors
protection.removeEditors(['email3','email4']);
}
Hope that helps!
Adding on #KRR's answer.
I changed the script to be dynamic.
function setProtection() {
var allowed = ["example#gmail.com,exmaple2#gmail.com"];
addProtection("Sheet1","A1:A10",allowed);
}
function editProtection(sheetname,range,allowed,restricted) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(sheetname);
var range = sheet.getRange(range);
//Remove previous protection on this range
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0;i<protections.length;i++) {
if (protections[i].getDescription() === sheetname + range){
protections[i].remove();
}
}
//Set new protection
var protection = range.protect().setDescription(sheetname + range);
// First remove all editors
protection.removeEditors(protection.getEditors());
// Add array of emails as editors of the range
if (typeof(allowed) !== "undefined") {
protection.addEditors(allowed.toString().split(","));
}
}
You can add as many options as you want and make them run onOpen. Set your variable and call editProtection as many times as you need.
You can get the emails dynamically from spreadsheet editors.
Also you might want to add another script to protect the whole sheet and set you as the owner.
Hope this helps.
It MUST be run as SCRIPT and NOT as an add-on.
If you have already locked your sheets and made your exceptions you can easily use Google's example code. We can use a for loop to find all the sheets and names. Then add a button to the script to load at start.
function FixPermissions() {
// Protect the active sheet, then remove all other users from the list of editors. Get all sheets in the workbook into an array
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
//Use a for loop to go through each sheet and change permissions and label it according to the name of the sheet
for (var i=0; i < sheets.length; i++) {
var name = sheets[i].getSheetName()
var protection = sheets[i].protect().setDescription(name);
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
}
}
//A special function that runs when the spreadsheet is open, used to add a custom menu to the spreadsheet.
function onOpen() {
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Fix Permission', functionName: 'FixPermissions'}
];
spreadsheet.addMenu('Permissions', menuItems);
}
Now in the menu bar you will see a new item when you reload/load the spreadsheet labeled Permissions
I've looked everywhere and it seems that GAS hasn't caught up with Google Spreadsheet. Is there a brute force method for setting protection on certain ranges in a sheet..? (And making a protected sheet with all the formulas and referring to them won't help me.)
I found this through google: https://code.google.com/p/google-apps-script-issues/issues/detail?id=1721
I even commented at the bottom. (More of a complaint than anything useful.) But the guy above me there posted this code:
//Function to Protect Target Sheet
function ProtectTargetSheet() {
//Enter ID for each Worksheet
var IDs = ["Sheeet_1", "Sheet_2"]
//Enter Page to protect, in order of WorkSheets
var Sheet_names = ["Page_1", "Page_2"]
//For each sheet in the array
for ( i = 0; i < IDs.length; i ++) {
//id of sheet
var sheet = IDs[i]
//activate dedicated sheet
var ActiveSheet = SpreadsheetApp.openById(sheet)
//Find last row and column for each sheet
var LastRow = ActiveSheet.getSheetByName(Sheet_names[i]).getLastRow();
var LastCol = ActiveSheet.getSheetByName(Sheet_names[i]).getLastColumn();
//Name of protectSheet
var Name = "Protect_Sheet";
//Range for Protection
var Named_Range = ActiveSheet.getSheetByName(Sheet_names[i]).getRange(1, 1, LastRow, LastCol);
//Impletment Protect Range
var protected_Range = ActiveSheet.setNamedRange(Name, Named_Range);
}
}
I don't see how this can work to give protection to a range when shared. It seems that it would just create a Named Range. He does say to set the permissions manually first. But I can't figure what exactly he meant.
Anyways, I was hoping that someone had found a way by now to do this until Google syncs GAS with its counterpart.
My wish is to, through 100% code, select a range in a sheet, within a spreadsheet, and make it so that when I share this whole spreadsheet to a person, that he or she can't edit that range. There will be other parts in that sheet that they have to be able to edit. Just not that range. It is easy to do this manually, but when having to create 100s of spreadsheets, it would be help to be able to do this through GAS.
Thanks.
Part of your question asked about protecting a sheet. Please have a look here: setProtected(protection)
As for programmatically protecting a range no. However, you could protect a sheet, does not need to be in the same spreadsheet and then create an onEdit trigger that would replace any change in your "protected" range with the original source data.
Something like this:
function onLoad() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var protected = ss.getSheets()[1].getRange("A1:B2").getValues(); //You could use openByID to get from a different ss
var target = ss.getSheets()[0].getRange("A1:B2").setValues(protected);
}
function onEdit(){
onLoad();
}
Every time a change is made to the spreadsheet the script will rewrite the data in the sheet for the range you specify.
The easiest approach I've found is to use Data Validation: that is, write a script which will examine each cell to be 'protected' and create and apply a validation rule which enforces entry of the existing content and rejects anything else. [Of course, this also implies that you have designed your spreadsheet such that all entered data is on sheet or sheets separate from those which have formula embedded. On these you use normal sheet protection.]
According to Control protected ranges and sheets in Google Sheets with Apps Script, posted on February 19, 2015, now is possible to protect a Google Sheets range.
From https://developers.google.com/apps-script/reference/spreadsheet/protection
Class Protection
Access and modify protected ranges and sheets. A protected range can
protect either a static range of cells or a named range. A protected
sheet may include unprotected regions. For spreadsheets created with
the older version of Google Sheets, use the PageProtection class
instead.
// Protect range A1:B10, then remove all other users from the list of editors.
var ss = SpreadsheetApp.getActive();
var range = ss.getRange('A1:B10');
var protection = range.protect().setDescription('Sample protected range');
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
If you are sending 100s of copies of the same sheet. Then create a template sheet, protect the ranges in it manually and send a copy of the template. It will retain the protection.
Sorry but as others have said there is not script method of setting protection at a sub-sheet level.