I'm using a code to duplicate a template sheet with multiple range protections within it,
Currently, I'm running the script to create one by one the new tabs. Can anyone help me with the following script to be able to duplicate the template many times at once (Example create from Template Sheet the following tabs: A,B,C,D,E,F) in one go:
Script
function duplicateSheetWithProtections() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
sheet = ss.getSheetByName('Template');
sheet2 = sheet.copyTo(ss).setName('A');
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var p = protections[i];
var rangeNotation = p.getRange().getA1Notation();
var p2 = sheet2.getRange(rangeNotation).protect();
p2.setDescription(p.getDescription());
p2.setWarningOnly(p.isWarningOnly());
if (!p.isWarningOnly()) {
p2.removeEditors(p2.getEditors());
p2.addEditors(p.getEditors());
}
}
}
Thank you for your help
How about this modification? I think that you can achieve what you want using Sheets API. The flow of this sample script is as follows.
When you use this script, please enable Sheets API at Advanced Google Services and API console. You can see about how to enable Sheets API at here.
Flow:
Set copied sheet names. For example, those are ["A", "B", "C",,]. This is from your question.
Copy template sheet using the sheet names.
Retrieve protected ranges from Template sheet using Sheets API.
Create request body.
Set protected ranges to copied sheets using Sheets API.
By this flow, the protected ranges of Template sheet can be copied to the all copied sheets by only one API call.
Modified script:
function duplicateSheetWithProtections() {
var sheetNames = ["A", "B"]; // Please set copied sheet names here.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = ss.getId();
var sheet = ss.getSheetByName('Template');
// Copy template sheet.
var copiedSheetIds = sheetNames.map(function(e) {return sheet.copyTo(ss).setName(e).getSheetId()});
// Retrieve protected ranges from Template sheet.
var sheets = Sheets.Spreadsheets.get(spreadsheetId).sheets;
var templateSheet = sheets.filter(function(e) {return e.properties.title == "Template"});
var protectedRanges = templateSheet[0].protectedRanges;
// Create request body.
var resources = copiedSheetIds.map(function(e) {
return protectedRanges.map(function(f) {
var obj = JSON.parse(JSON.stringify(f));
delete obj.protectedRangeId;
if (obj.warningOnly) delete obj.editors;
obj.range.sheetId = e;
return {"addProtectedRange": {"protectedRange": obj}};
});
});
resources = Array.prototype.concat.apply([], resources);
// Set protected ranges to copied sheets
Sheets.Spreadsheets.batchUpdate({"requests": resources}, spreadsheetId);
}
References:
spreadsheets.get
spreadsheets.batchUpdate
If I misunderstand your question, please tell me. I would like to modify it.
Related
Looking to add a few functions together but having trouble with it. My goal is to compare two sheets find the duplicate names and send that data to a new spreadsheet in a specific folder. Right now I have a Spreadsheet that has List, Original, and Filter. The List is where I'm looking to find duplicates compared to the Original list and the filter is how I want it to look at the end when the new sheet is created. I'm currently using the formula
=filter(Original!A:F, match(Original!C:C,List!B:B,0)) to achieve the filter and am looking to convert that into google scripts. From the Filter tab I can create the spreadsheet and send it to a specific folder but since it's a formula it'll copy the formula instead of the values. I've heard of using {contents, text only} but I would prefer to have one script that filters, matches, and creates the spreadsheet
Below is the link to the spreadsheet for you guys to check out. Sorry for the long post... Thanks in Advance!
https://docs.google.com/spreadsheets/d/1i5ejG0ldB9EIMtaFezbsJNJS5hbdu9RpyJ4134b9q7s/edit#gid=1328732453
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('Send Sheet');
menu.addItem('Create Sheet', 'createSheet')
menu.addToUi();
}
function createSheet(){
var sheet = SpreadsheetApp.getActiveSheet(); // Get current active sheet.
var sheet_name = sheet.getRange("J1").getValue(); // Get the value of cell B1, used to name the new spreadsheet.
var folder = DriveApp.getFolderById("xxxxxxxxx"); // Get the ID of the folder where you will place a copy of the spreadsheet.
var newSS = SpreadsheetApp.create(sheet_name); // create new blank spreadsheet in a root folder
var asFile = DriveApp.getFileById(newSS.getId()); // get new spreadsheet as a file
folder.addFile(asFile); // add this file to destination folder
DriveApp.getRootFolder().removeFile(asFile); // remove a file from root folder
var copiedSheet = sheet.copyTo(newSS); // copy active sheet to new spreadsheet
copiedSheet.setName(sheet_name); // rename copied sheet
newSS.deleteSheet(newSS.getSheetByName('Sheet1')); // remove "Sheet1" sheet which was created by default in new spreadsheet
}
function filter() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var trn = ss.getSheetByName("Original");
var originalData = trn.getRange(2, 1, trn.getLastRow()-1,7).getValues();
var person = "Jonathan";
var data = originalData.filter(function(item){ return item[0] === person });
var targetSheet = ss.insertSheet(person);
targetSheet.getRange(2, 1, data.length, data[0].length).setValues(data);
}
{contentsOnly, true} only works for the method range.copyTo(), not for sheet.copyTo()
As for the method range.copyTo(), unfortunately you cannot use it to copy data into a different spreadsheet.
As a workaround, you can first create an empty sheet in the new spreadsheet, and the copy the data with getValues() and setValues()
Sample:
Modify
var copiedSheet = sheet.copyTo(newSS); // copy active sheet to new spreadsheet
copiedSheet.setName(sheet_name);
to
var newSheet = newSS.insertSheet();
newSheet.setName(sheet_name);
var dataRange = sheet.getDataRange();
var notation = dataRange.getA1Notation();
var values = dataRange.getValues();
newSheet.getRange(notation).setValues(values):
I have a Google sheet with almost 160 sub sheets. All the questions are in "All" sheet and B column is the actual spreadsheet name where they should be. The following code is reading data from "All" spreadsheet and sending them perfectly to the last row of desired spreadsheet perfectly but it is taking very long time! Probably because it has a lot of subsheets and is using getSheetByName again and again. Now I've stored all the sub sheets' name and ID in 'sheets' and 'sheetID' arrays at once. I'm thinking to compare between rangeValues[j][1] and sheetNames[k][0]. Below is the code and screenshot of the spreadsheet.
Is this a suitable way? Please help me to run the script faster!
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("All");
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 8);
var curr_sheet;
function send() {
var rangeValues = searchRange.getValues();
var sheetNames = new Array();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheets.length ; i++) sheetNames.push( [ sheets[i].getName() ] );
//Logger.log (sheetNames.length);
var sheetID = new Array();
var sheetIDs = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheetIDs.length ; i++) sheetID.push( [ sheetIDs[i].getSheetId() ] );
//Logger.log (sheetID.length);
for ( j = 0 ; j < lastRow; j++)
{
for ( k = 0 ; k < sheetNames.length ; k++) //
{
if (rangeValues[j][1] === sheetNames[k][0])
{
var targetSheet = ss.getSheetByName(sheetNames[k][0]); // This line is working but taking very long to complete
var targetSheet = ss.getSheetByID(sheetIDs[k][0]); // This line is not code just to show what I'm thinking to do.
targetSheet.getRange(targetSheet.getLastRow()+1, 1, 1, 8).setValues([rangeValues[j]]);
}
}
}
}
I believe your goal as follows.
You want to reduce the process cost of your script.
Modification points:
There is no method of getSheetByID in Class Spreadsheet. By this, I think that in your case, the following script can be removed.
var sheetID = new Array();
var sheetIDs = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheetIDs.length ; i++) sheetID.push( [ sheetIDs[i].getSheetId() ] );
In your script, getSheetByName and getRange(###).setValues(###) are used in the loop. And also, even when the sheet is the same sheet, each row is put using setValues in the loop every row. In this case, the process cost will be high. I think that these situation might be the reason of your issue.
Fortunately, in your situation, all sheets are in the same Spreadsheet. So for reducing the process cost of your script, here, I would like to propose to put the values to each sheet using Sheets API. The flow of modified script is as follows.
Retrieve Spreadsheet and values.
This script is from your script.
Retrieve all sheets in the Spreadsheet.
Check whether the sheet names from the column "B" are existing.
Create the object for putting to each sheet using Sheets API.
Request the method of spreadsheets.values.batchUpdate of Sheets API using the created object.
When above points are reflected to your script, it becomes as follows.
Modified script:
Please copy and paste the following script. And, please enable Sheets API at Advanced Google services. And then, please run the script.
function send() {
// 1. Retrieve Spreadsheet and values. This script is from your script.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("All");
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 8);
var curr_sheet;
var rangeValues = searchRange.getValues();
// 2. Retrieve all sheets in the Spreadsheet.
var sheets = ss.getSheets().reduce((o, e) => Object.assign(o, {[e.getSheetName()]: e}), {});
// 3. Check whether the sheet names from the column "B" are existing.
// 4. Create the object for putting to each sheet using Sheets API.
var data = rangeValues.reduce((o, e) => {
if (sheets[e[1]]) {
if (o[e[1]]) {
o[e[1]].values.push(e);
} else {
o[e[1]] = {range: `'${e[1]}'!A${sheets[e[1]].getLastRow() + 1}`, values: [e]};
}
}
return o;
}, {});
// 5. Request the method of spreadsheets.values.batchUpdate of Sheets API using the created object.
Sheets.Spreadsheets.Values.batchUpdate({data: Object.values(data), valueInputOption: "USER_ENTERED"}, ss.getId());
}
Note:
In this case, when you run the script, one API call is used. Because the method of spreadsheets.values.batchUpdate is used.
In my environment, when I tested above script and your script using a sample Spreadsheet, I could confirm the reduction of the process cost of about 70 % from your script.
References:
Advanced Google services
Method: spreadsheets.values.batchUpdate
reduce()
Try this:
Should be a lot faster
function send() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("All");
var rg=sh.getRange(1,1,sh.getLastRow(),8);
var v=rg.getValues();
var names=[];
var sheets=ss.getSheets();
sheets.forEach(function(sh,i){names.push(sh.getName());});
v.forEach(function(r){
let idx=names.indexOf(r[1]);//column2
if(idx!=-1) {
sheets[idx].getRange(sheets[idx].getLastRow()+1,1,1,8).setValues([r]);
}
});
}
Here is my function:
function deleteNamedRangeWithREF() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = ss.getId();
var spreadsheetObject = Sheets.Spreadsheets.get(spreadsheetId);
var namedRanges = spreadsheetObject.namedRanges;
Logger.log(namedRanges);
var namedRangesRef = [];
for(var i = 0; i < namedRanges.length; i++)
{
if((namedRanges[i].range.endColumnIndex - namedRanges[i].range.startColumnIndex) <= 0 || (namedRanges[i].range.endRowIndex - namedRanges[i].range.startRowIndex) <= 0)
namedRangesRef.push(namedRanges[i].namedRangeId);
}
var BUrequest = Sheets.newBatchUpdateSpreadsheetRequest();
BUrequest.requests = [];
for(var i = 0; i < namedRangesRef.length; i++)
{
var request = {
deleteNamedRange: {
namedRangeId: namedRangesRef[i]
}
}
BUrequest.requests.push(request)
}
if (BUrequest.requests.length)
Sheets.Spreadsheets.batchUpdate(BUrequest, spreadsheetId);
}
I'm using sheets API to delete bad #REF named ranges.
When coding this, this worked, but now looks like it's broken.
You can see by yourself #Logger.log(namedRanges); all listed named ranges are valid, it doesn't retrieve the #REF named ranges.
Did google changed something, is there an alternative?
At first, I have to apologize and modify my comments in your question. At Sheets API, when there is no value of object, the property is not created. For example, sheetId of gid=0 is not shown in the range object. (But I'm not sure whether it occurs at all response patterns of Sheets API.) About this, I had forgot it. I have to apologize for this situation.
By considering above situation, I investigated your sample Spreadsheet. As the results, it was found as follows.
NamedRange with PlageNommée1 could be retrieved by Sheets API. In this case, although the range is included as the grid range, sheetId is not included in the range object because the sheet ID is 0.
NamedRange with PlageNommée2 couldn't be retrieved by Sheets API. In this case, the range object related to PlageNommée2 couldn't be retrieved, although sheetId of PlageNommée2 is not 0.
I think that when the grid range is #REF!, Sheets API might use this as no namedRange.
From above situation, I thought that Spreadsheet Service instead of Sheets API might be able to retrieve values. So I tested this. The sample script using Spreadsheet Service is as follows.
Sample script:
var id = "### spreadsheetId ###"; // Please set this.
var namedRanges = SpreadsheetApp.openById(id).getNamedRanges();
for (var i = 0; i < namedRanges.length; i++) {
Logger.log("name: %s, range: %s!%s",
namedRanges[i].getName(),
namedRanges[i].getRange().getSheet().getSheetName(),
namedRanges[i].getRange().getA1Notation()
)
}
Result:
name: "PlageNommée1", range: "Feuille 1!A1"
name: "PlageNommée2", range: "Feuille 2!#REF!"
In this case, the range of Feuille 2 can be retrieved as #REF!. From this result, you can check the namedRanges.
It was found that the namedRange of PlageNommée2 could be retrieved by Spreadsheet Service, even if the range is #REF!.
Reference:
getNamedRanges()
I've read through this post >> set format as plain text
I'm creating a new thread because that thread is a few years old already and I didn't want to revive something from a few years ago.
the code:
function A1format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mainsheet = ss.getSheetByName("Sheet1");
var G = mainsheet.getRange("C15:BH3000").getGridId();
var illa = mainsheet.getRange("A13");
Logger.log(G);
illa.copyFormatToRange(G, 16, 3,200, 30);
}
This code is supposed to set plain text format for the sheet named Sheet1
I've tried var mainsheet = ss.getSheetByName("Sheet1, Sheet2, Sheet3"); but this doesn't seem to work, I just get an error message.
This is the current code I have, this code works but is both inefficient and a real pain to maintain if something changes:
function setPlainTextDefault() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheets()[0];
var sheet2 = ss.getSheets()[2];
var sheet3 = ss.getSheets()[4];
var sheet4 = ss.getSheets()[7];
var sheetColumn1 = sheet1.getRange("A1:A");
var sheetColumn2 = sheet2.getRange("A1:A");
var sheetColumn3 = sheet3.getRange("A1:A");
var sheetColumn4 = sheet4.getRange("A1:A");
sheetColumn1.setNumberFormat("#");
sheetColumn2.setNumberFormat("#");
sheetColumn3.setNumberFormat("#");
sheetColumn4.setNumberFormat("#");
}
Here I am changing each column A in every sheet to plain text by using the index number of the sheet, so I have to manually count the number for every sheet, this is a nightmare as I have a very large number of sheets, it will take too much time for me to manually count the sheets and then add it to my current code. I know there is a better more efficient way of doing this, but I don't know how due to my lack of knowledge in google apps scripting.
How do you do this for every sheet in the document regardless of how many sheets are present? I want to go through every sheet, from sheet1 till x number of sheets and then change every column A to plain text.
var mainsheet = ss.getSheetByName("Sheet1, Sheet2, Sheet3") throws an error because there isn't a sheet named "Sheet1, Sheet2, Sheet3". "There isn't a way to reduce the calls to the Spreadsheet Service on your code, but you could make that it "looks better" by using some JavaScript features like loops and array handling.
Example (untested)
On the following code snippet, arrays, a for and indexOf are used to reduce a bit the number of lines of the original code.
function setPlainTextDefault() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sheetsToProcess = [0,2,4,7]; // Array holding the sheets indexes to process
for(var i = 0; i < sheets.length; i++){
/* If i is not in the sheetsToProccess indexOf returns -1 which is parsed as false
otherwise the result is parsed as true */
if(sheetsToProcess.indexOf(i)){
var sheet = ss.getSheets()[i];
var sheetColumn = sheet.getRange("A1:A");
sheetColumn.setNumberFormat("#");
}
}
}
To make the above work for every sheet, comment out or remove if(sheetsToProcess.indexOf(i)){ and the corresponding }.
It's worth to note that if you are looking help to find the "best" way to improve your code, you could try Code Review.
This was the code I needed:
function setPlainText() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
for(var i = 0; i < sheets.length; i++) {
//Logger.log(sheets[i].getName());
var setPlainText = ss.getSheets()[i];
var sheetColumnA = setPlainText.getRange("A1:A");
sheetColumnA.setNumberFormat("#");
var sheetColumnB = setPlainText.getRange("B1:B");
sheetColumnB.setNumberFormat("#");
}
}
It will:
Iterate through a document with x number of sheets however big or small
Then for each iteration, set plain text format for every column A and B for every sheet in the document
I have following google spreadsheet bound script whose job is to create copy of the 'template' sheet whenever there is data in the A:A column of the 1st Sheet ('Projects').
This script was working absolutely fine. However, later I have added a line of code with this script (the last line of the code in the script below) intending to extract and put the newly created sheet's name in one of its cell - J2.
Somehow that line of code is not working while the rest of the code does its job properly. Any suggestion on this would be greatly appreciated. Thank you!
function onOpen() {
var menu = [{
name : "Add",
functionName : "newSheet"
}];
SpreadsheetApp.getActiveSpreadsheet().addMenu("Sheet", menu);
}
function newSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("Template");
var sheet1 = ss.getSheetByName("Projects")
var getNames = sheet1.getRange("A:A").getValues().filter(String).toString().split(",");
for (var i = 0; i < getNames.length; i++) {
var copy = ss.getSheetByName(getNames[i]);
if (copy) {
Logger.log("Sheet already exists");
} else {
templateSheet.copyTo(ss).setName(getNames[i]);
ss.setActiveSheet(ss.getSheetByName(getNames[i]));
ss.moveActiveSheet(ss.getNumSheets());
ss.getSheetByName(getNames[i]).copyTo(ss.getSheetByName(getNames[i]).getRange("J2"));
}
}
}
According to GAS documentation, the copyTo() method of the Sheet class only accepts Spreadsheet instances as parameters. It doesn't work because you are passing Range as parameter. Also, getSheetByName() returns the Sheet object, so you aren't actually copying the name of the sheet.
If you'd like to modify specific range, it's much more convenient to use Range instead.
var sheet = ss.getSheetByName(getNames[i]);
var cell = sheet.getRange("J2");
cell.setValue(getNames[i]); // writing the name of the sheet to cell J2