Creating New Spreadsheets from Spreadsheet - google-apps-script

I am creating a sheet that can create a series of sheets that interact to help run a simulation. Part of this project involves creating a series of sheets- individual score sheets- that can only be accessed by single players in the game. The code I'm hoping to use is below, but I'm running into a series of issues. The first is thatit says I don't have permission to run SpreadsheetApp.create. I have spent about an hour looking around online for how to cure this, but just don't understand it and can't find a good answer. The second, and potentially broader issue, is that it seems incredibly complicated to use GAS to interact with different spreadsheets (i.e. not within the same spreadsheet). At a later point in the project, we will have to retrieve information from these sheets, etc. and if it is going to be difficult to do through scripts, may have to think of a workaround.
/** #OnlyCurrentDoc */
function individualSheets(){
var playerarray = playerArray();
var spreadsheet = SpreadsheetApp.getActive();
var spreadsheetlinks = []
//creates individual sheet for each player, then adds the link of their sheet to a list
for(i=0;i<playerarray.length;i++){
var spreadsheetname = playerarray[i];
var newspreadsheet = SpreadsheetApp.create(spreadsheetname);
spreadsheetlinks.push(DriveApp.getUrl(newspreadhseet));
}
//pasting the links of each player in the appropriate sheet
for(i=0;i<spreadsheetlinks.length;i++){
spreadsheet.setActiveSheet("PlayerInfo").getRange('D'+(i+1)).activate()
spreadsheet.getCurrentCell.setValue(spreadhseetlinks[i])
}
}

Apps Script doesn't have difficulty to use several Spreadsheets in the same script.
I would add another array to store the Ids, so you won't have to get them from the urls:
var spreadsheetlinks = []
var spreadsheetIds = [];
//creates individual sheet for each player, then adds the link of their sheet to a list
for(i=0;i<playerarray.length;i++){
var spreadsheetname = playerarray[i];
var newspreadsheet = SpreadsheetApp.create(spreadsheetname);
spreadsheetlinks.push(DriveApp.getUrl(newspreadhseet));
spreadsheetIds.push(newspreadsheet.getId);
}
You can open the Spreadsheets using openById, so you can use an unlimited amount of them:
var sprSheet1 = SpreadsheetApp.openById('id 1');
var sprSheet2 = SpreadsheetApp.openById('id 2');
var sprSheet3 = SpreadsheetApp.openById('id 3');
...
or even better:
var sprSheets = [];
for (var j = 0; j < spreadsheetIds.length; j++){
sprSheets.push(SpreadsheetApp.openById(spreadsheetIds[j]);
}
Regarding the SpreadsheetApp.create issue, make sure you authorized the needed scopes:
Scripts that use this method require authorization with one or more of
the following scopes:
https://www.googleapis.com/auth/spreadsheets.currentonly
https://www.googleapis.com/auth/spreadsheets
You can check them in Apps Script View > Manifest file. If you never touched this, they won't show up. You can add them manually:
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive.file"
]
In case you still get the permission issue when creating the Spreadsheet, you might want to check the playerArray function to see if it's actually returning a String.

Related

Optimizing Script run time gathering large volume of spreadsheet tab information

I'm hoping that someone might have an alternative suggestion for a strategy for the Google Apps Script I wrote below to help it finish running before it times out. I have a lengthy list of Spreadsheets (about 200) that each have anywhere from 20-100 individual sheets/tabs in them. I'm trying to loop through the entire list of spreadsheets to gather information about each individual sheet (about 8,000 in total) but unfortunately it's running to slow to complete in one run.
Thanks in advance for any suggestions
function getListofTabs(urlName) {
var ssRecipeTable = SpreadsheetApp.openByUrl(urlName);
var tabs = ssRecipeTable.getSheets();
for(var i=0;i<tabs.length;i++){
sheetName = tabs[i].getName();
sheetID = tabs[i].getSheetId();
sheetURL = tabs[i].getParent().getId();
//Logger.log(sheetName,sheetID,sheetURL);
dataList.appendRow(['=HYPERLINK("https://docs.google.com/spreadsheets/d/'+tabs[i].getParent().getId()+'/edit#gid='+tabs[i].getSheetId()+'","'+tabs[i].getName()+'")',tabs[i].getName(),tabs[i].getParent().getId(),tabs[i].getSheetId()]);
}
}
//Function to feed getListofTabs function with list of all recipe workbooks
function getArray(){
//Temp location for active tests
var recipeDataSS = SpreadsheetApp.getActiveSpreadsheet();
//List of all Recipe workbooks from master Tested Recipe folder
var listofWorkbooks = recipeDataSS.getSheetByName('ListofWorkbooks');
//Location for data input sheet for Plx Upload
var dataList = recipeDataSS.getSheetByName('Spreadsheets2');
//gets list of workbooks urls, loops through until the end
var dataRecipe = listofWorkbooks.getDataRange().getValues();
for(var x=0; x<dataRecipe.length; x++)
{
var urlName = dataRecipe[x][0];
getListofTabs(urlName);
}}```
You should get rid of appendRow() in the for loop.
In order to do that you need to create an empty array before the for loop and then push() the new values into this array. Then after the for loop, use setValues() to add the values to the sheet. In this way, you call setValues() once instead of calling appendRow() tabs.length number of times.
For example the function getListofTabs(urlName) could be replaced by that:
function getListofTabs(urlName) {
var ssRecipeTable = SpreadsheetApp.openByUrl(urlName);
var dat = []
var tabs = ssRecipeTable.getSheets();
for(var i=0;i<tabs.length;i++){
sheetName = tabs[i].getName();
sheetID = tabs[i].getSheetId();
sheetURL = tabs[i].getParent().getId();
//Logger.log(sheetName,sheetID,sheetURL);
dat.push(['=HYPERLINK("https://docs.google.com/spreadsheets/d/'+tabs[i].getParent().getId()+'/edit#gid='+tabs[i].getSheetId()+'","'+tabs[i].getName()+'")',tabs[i].getName(),tabs[i].getParent().getId(),tabs[i].getSheetId()]);
}
dataList.getRange(dataList.getLastRow()+1,1,dat.length,dat[0].length).setValues(dat);
}
Please read carefully the best practices when writing GAS.

DUPLICATING interconnected (via IMPORTRANGE) spreadsheets and CHANGING the IMPORTRANGE formulas both via Google Apps Script

Skip this paragraph, if you do not care about the background and immediately want to see the coding question.
I am pretty new to coding and this is my first question I post here, but I hope I can make some sense. I searched the web thoroughly, but I cannot find an answer to my probably not so special problem.
Some background: I work as a coach and have google spreadsheets with training plans that I give to clients. Since I love coding and google spreadsheets, the statistics and functionality I programmed quickly developed, so much, I had to split the spreadsheet and its up to 7 training day tabs/sheets in different spreadsheets, so I save my clients or rather their smartphones some working memory. So far so good.
I programmed an apps script that duplicates the master/setup sheet as well as the training days spreadsheets (#1 to #7) and customizes the folder they are saved in as well as the copies names with the clients nickname/first name.
Today I also managed to successfully replace the importranges in the calender sheet (background: so I can see when my clients trained which day) of my master/setup spreadsheet, so that the copy of the master spreadsheet, which I just duplicated, does not link to the training day #1, #2, ... template, but the copy of the training day #1,#2 ... template. I do not know how comprehensible this is (also english is not my first language) so I try to rephrase it: Of course the IMPORTRANGE formulas that interconnect all the template spreadsheets with each other do not update when duplicating, but still refer to the template spreadsheets.
However, I asked myself why hardcode everything (the actual cell ranges and stuff) and if there is not an easy possibility to always substitute the id part of all importrange functions (of template #1 for example) with the id of the sheet (which I already stored in an array).
Do you understand what I mean and could you help me with this?
Here is what I already coded so far.
Thanks to anyone who read this question and trys to help!
PS: Also I am happy if someone wants to correct or simplify my current code, but I am happy that it works so far and I would be overwhelmed if someone could push me in the right direction of how to achieve this.
PPS: The reason why I am leaning in the direction of a GAS version of find and replace all importrange functions would be, that I would keep some flexibility in my spreadsheet template architecture and I would be able to edit the template in the future without thinking about hardcoded ranges in the GAS code, that I might destroy.
Tl;Dr: Is there an easy way to use find and replace via GAS to just change the id part of the importrange formulas after duplicating spreadsheets?
//function that creates a new menu in the spreadsheet just right of the "help" tab
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Scripts')
.addItem('Create Duplicates', 'createDuplicates')
.addItem('Create Links in Master Spreadsheet', 'createLinksInMasterSheet')
.addToUi();
}
const parentFolder = DriveApp.getFolderById("ID OF PARENT FOLDER");
const nameOfMasterSheet = "TrnngPln";
const currentVersion = "4.0";
//function that creates duplicates of master sheet, all training days in a new folder that is named after the clients nickname
function createDuplicates() {
const clientNickname = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SETUP").getRange('AB2').getValue();
const currentmonth = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("InfoForAppsScript").getRange('F2').getValue();
const currentyear = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("InfoForAppsScript").getRange('G2').getValue();
const folderNewClient = parentFolder.createFolder('TP.'+clientNickname+'.['+currentmonth+'/'+currentyear+']');
const newMasterSpreadSheetName = ''+clientNickname+'.'+nameOfMasterSheet+'['+currentmonth+'/'+currentyear+'.]'+currentVersion+'';
const setup = DriveApp.getFileById("ID OF SETUP TEMPLATE");
var newMasterID = setup.makeCopy(newMasterSpreadSheetName,folderNewClient).getId();
const nameOfCopyOfDay1 = '#1.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day1 = DriveApp.getFileById("ID OF TEMPLATE DAY 1");
var newDay1id = day1.makeCopy(nameOfCopyOfDay1,folderNewClient).getId();
const nameOfCopyOfDay2 = '#2.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day2 = DriveApp.getFileById("ID OF TEMPLATE DAY 2");
var newDay2id = day2.makeCopy(nameOfCopyOfDay2,folderNewClient).getId();
const nameOfCopyOfDay3 = '#3.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day3 = DriveApp.getFileById("ID OF TEMPLATE DAY 3");
var newDay3id = day3.makeCopy(nameOfCopyOfDay3,folderNewClient).getId();
const nameOfCopyOfDay4 = '#4.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day4 = DriveApp.getFileById("ID OF TEMPLATE DAY 4");
var newDay4id = day4.makeCopy(nameOfCopyOfDay4,folderNewClient).getId();
const nameOfCopyOfDay5 = '#5.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day5 = DriveApp.getFileById("ID OF TEMPLATE DAY 5");
var newDay5id = day5.makeCopy(nameOfCopyOfDay5,folderNewClient).getId();
const nameOfCopyOfDay6 = '#6.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day6 = DriveApp.getFileById("ID OF TEMPLATE DAY 6");
var newDay6id = day6.makeCopy(nameOfCopyOfDay6,folderNewClient).getId();
const nameOfCopyOfDay7 = '#7.'+clientNickname+'['+currentmonth+'/'+currentyear+']'+currentVersion+'';
const day7 = DriveApp.getFileById("ID OF TEMPLATE DAY 7");
var newDay7id = day7.makeCopy(nameOfCopyOfDay7,folderNewClient).getId();
}
//function that sets Links in Calendar tab of master trainingplan spreadsheet
function createLinksInMasterSheet() {
var searchFor = ['title contains ".TrnngPln"','title contains "#1."','title contains "#2."','title contains "#3."','title contains "#4."','title contains "#5."','title contains "#6."','title contains "#7."'];
var ss = SpreadsheetApp.getActive(); //current spreadsheet
var directParents = DriveApp.getFileById(ss.getId()).getParents(); // folderIterator "targets" parent folder
var folder = directParents.next(); //accesses parent folder
var names =[]; //array that stores names of the spreadsheets found in folder in the order of the array variable searchFor
var fileIds=[]; //array that stores the IDs of the spreadsheets found in folder in the order of the array variable searchFor
for (var i=0; i<searchFor.length; i++) { //forLoop that goes through the searchFor array
var files = folder.searchFiles(searchFor[i]); //creates file iterator t
while (files.hasNext()) { //while loop goes through all the files that match searchFor variable
var file = files.next(); //
var fileId = file.getId(); // To get FileId of the file
fileIds.push(fileId); //
var name = file.getName(); //To get name of the file
names.push(name); //
}
}
for (var i=1; i<fileIds.length;i++) {
const formula = 'IMPORTRANGE("'+fileIds[i]+'";"#'+i+'!JJ188:JX188")'; //tak
var rownumber = 265+i;
Logger.log(rownumber);
ss.getSheetByName('Cal').getRange('C'+rownumber+'').setFormula(formula);
Logger.log(formula);
}
}
PS: Most of this is not my code but edited code I found somewhere on this board, youtube or wherever and edited it to fit my purpose.
From the question
Tl;Dr: Is there an easy way to use find and replace via GAS to just change the id part of the importrange formulas after duplicating spreadsheets?
There are several ways to replace the first argument (id / key / url) on IMPORTRANGE functions by using Google Apps Script. Perhaps the easier is to use Class TextFinder as it has several methods that might be helpful like:
matchFormulaText(matchFormulaText)
replaceAllWith(replaceText)
useRegularExpression(useRegEx)

Collapse all groups of certain depth in Google Sheet

I am trying to create a google sheet that allows the view of several levels of details.
The idea is to use the macro feature to use shortcuts that run one macro for each level of detail. Each level of detail shows the active sheet with at a certain depth of groups.
i.e. when I run the macro for the most top level view all row groups are collapsed:
function _1stLevelofDetail() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getActiveSheet().collapseAllRowGroups();
};
when I run the macro for the most detailed view (in my case the 4th) all row groups are expanded:
function _4thLevelofDetail() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getActiveSheet().expandAllRowGroups();
};
How can I change the following code so it is robust enough that I can update/add/remove row columns without braking the code
function _3rdLevelofDetail() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getActiveSheet().expandAllRowGroups();
spreadsheet.getActiveSheet().getRowGroup(8, 3).collapse();
spreadsheet.getActiveSheet().getRowGroup(11, 3).collapse();
spreadsheet.getActiveSheet().getRowGroup(14, 3).collapse();
spreadsheet.getActiveSheet().getRowGroup(18, 3).collapse();
spreadsheet.getActiveSheet().getRowGroup(23, 3).collapse();
};
My idea is to get all row groups of depth 3 but I don't know how to put that into code.
The problem with your code is that Spreadsheet operations are sometimes bundled together to improve performance
This means that e.g. spreadsheet.getActiveSheet().getRowGroup(8, 3).collapse(); is being called together with spreadsheet.getActiveSheet().expandAllRowGroups(); before the former finished executing.
This problem can be solved by implementing SpreadsheetApp.flush(); after each request that might be a bit time consuming when it is critical that it finishes before the next line of code will be executed.
So, e.g.
spreadsheet.getActiveSheet().expandAllRowGroups();
SpreadsheetApp.flush();
spreadsheet.getActiveSheet().getRowGroup(8, 3).collapse();
UPDATE:
If you want to dynamically collapse all row groups of a certain depth, you need to retrieve them first.
For this, you need to use the Advanced Sheets Service, which allows you to use the methods of the Sheets API in Apps Script.
After enabling the Advanced Sheets Service, you can collapse all groups with the desired depth as following:
function _3rdLevelofDetail() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getActiveSheet().expandAllRowGroups();
var Id = spreadsheet.getId();
var groups = Sheets.Spreadsheets.get(Id, {
ranges: spreadsheet.getActiveSheet().getSheetName(),
fields: "sheets/rowGroups"
});
var rowGroups = groups.sheets[0].rowGroups;
for (var i = 0; i < rowGroups.length; i++){
if (rowGroups[i].depth == 3){
var index = rowGroups[i].range.startIndex;
spreadsheet.getActiveSheet().getRowGroup(index, 3).collapse();
}
}
};

Google Sheets - Allow Users to run scripts on a protected range

I'm new to Apps Script and have been slowing teaching myself. I have searched Stack Over Flow and found similar questions and tried to work through the issue using the information found but thus far with no success. Hopefully someone here can give a newbie some guidance.
I have a spreadsheet that I am sharing with other users. There are a few ranges with formulas in them that I am trying to protect. I have written a script that can change the forumlas within the protected ranges. It runs perfectly under my account but the shared users are unable to run the script because they "don't have permission".
I have tried to write a bit of code that tries to grant temporary access to edit the protected ranges. My thought was to have three different functions. The first function removes the protections. The second function performs the changes to the now unprotected ranges. The third function then protects the ranges again. The code below works perfectly under my account. However when logged in as a shared user it causes an alert saying that "You do not have permission to perform.."
Can someone please tell me where I am going wrong and point me in the right direction?
function myFunction() {
//add protections back
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Spray Template');
var range = sheet.getRange('B15:E15');
var range2 = sheet.getRange('G15:H15');
var range3 = sheet.getRange('D16:E16');
var ranges = [range,range2,range3];
for (var j = 0; j<3; j++){
ranges[j].protect().removeEditor('email_here#gmail.com');
}
}
function myFunction2(){
//remove protections
var ss = SpreadsheetApp.getActive();
var ss2= ss.getSheetByName('Spray Template');
var protections = ss2.getProtections(SpreadsheetApp.ProtectionType.RANGE);
f
or (var i = 0; i < protections.length; i++) {
var protection = protections[i];
protection.addEditor('email_here#gmail.com');
SpreadsheetApp.flush();
protection.remove();
}
}
function test() {
myFunction2();
functionThatChangesRange();
myFunction();
}
I imagine that without permission to edit/access those protected ranges, other people will also lack permission to edit the protection itself (even through scripts).
This is because the script inherits its permissions from the user that is running it (ie the script runs under that person's account).
Obviously, you can't both protect a range, and also allow anyone to change the protection - that would defeat the whole purpose!
I saw it suggested elsewhere to leave one cell unprotected and setup a script within the document that runs 'onedit'. Whenever the user changes the value of that cell the script should run.
One can use onEdit to run the script or installable trigger. However, I have not identified a way in which a script can be called from a custom menu or a button and edit successfully protected ranges.

google-apps-script how to avoid repeating code?

I have 10 different google sheets. I have a very basic google apps script to apply to all of these. This is the script:
function myRecalculate() {
var ss = SpreadsheetApp.openById('sheet ID');
var sheet = ss.getSheetByName("Sheet7");
var cell = sheet.getRange('A1')
var number = Math.floor(Math.random() * 8) + 1;
cell.setValue(number)
}
And I want this to be used in 9 other sheets as well. Is there a way in GAS to avoid repeating this code 10 times? I could just copy paste and alter the sheet ID every time, but does not feel right.
Is there a simple solution?
edit. Solved it myself with packing all sheetID's in an array, and loop the function through the array:
function myRecalculate() {
var idBE = "id of spreadsheet1"
var idDE = "id of spreadsheet2"
var idFR = "id of spreadsheet3"
// pack them in array
var id = [idBE, idDE, idFR]
// loop through array, do something with sheet
for (i in id){
var ss = SpreadsheetApp.openById(id[i])
var sheet = ss.getSheetByName("Sheet7");
var cell = sheet.getRange('A1')
var number = Math.floor(Math.random() * 8) + 1;
cell.setValue(number)
}}
You can use libraries!
Here is the reference for Apps Script libraries: Libraries
Note: Code ran through libraries is not as fast as code ran directly from your project.
In short, you can create a version of your code by going to File -> Manage Versions. Save a new version. Then go to File -> Project Properties and copy the Project Key.
Go to your next Apps Script project, and go to Resources -> Libraries. Paste your project key into the Find a Library text field and click Select. Select the version of you're library that you want to use and hit save.
You can now reference the code in that Apps Script project like: MyProject.myRecalculate()