How to auto-create a sheet based on named range - google-apps-script

I want to be able to have the script create a new template sheet based on a named range from the base sheet. Also check for duplicate names and not create a new sheet if a duplicate exists.
function test() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Get the range of cells that store employee data.
var employeeDataRange = ss.getRangeByName("EmployeeRef");
var employeeObjects = employeeDataRange.getValues();
var template = ss.getSheetByName('Template');
for (var i=0; i < employeeObjects.length; i++) {
// Put the sheet you want to create in a variable
var sheet = ss.getSheetByName(employeeObjects[i]);
// Check if the sheet you want to create already exists. If so,
// log this and loop back. If not, create the new sheet.
if (sheet) {
Logger.log("Sheet " + employeeObjects[i] + "already exists");
} else {
template.copyTo(ss).setName(employeeObjects[i]);
}
}
return;

Related

Google Apps Script: Prevent duplicate copies based on two columns

I am working with some colleagues who want the following to happen within a Google Sheet:
A Google Form contains a question that asks which Counselor a student is assigned to (among other questions)
Forms are submitted throughout the year by students
When a form is submitted, the form data goes into a Google Sheet in a Responses sheet
The Counselors would like a copy of each row to appear in another sheet within the main Sheet, based on the name of the Counselor
In their own sheets, each Counselor needs to be able to manipulate the data (sorting, highlighting rows, adding notes to the row/submission) ←hence a copy is needed instead of a query
I have the following script that copies the rows in the correct Counselor sheet, and does not copy a row into a Counselor sheet if it already appears. However, if a Counselor modifies anything in the row, the script will make a duplicate row (with the original data) the next time it is run, perhaps because it sees the modified row as not an exact match.
Is there a way to modify my script so it can check against a unique part of a row in the Responses sheet (the columns at indexes 0 and 1 together in the same row create a unique entry) in any part of a Counselor sheet before it creates a copy? In other words, it would not create a duplicate row if the Counselor modifies anything except for columns 0 and 1.
function copyData() {
var formResponses = SpreadsheetApp.getActive().getSheetByName("Form Responses 1");
var formValues = formResponses.getDataRange().getValues();
formValues.shift(); // remove the header row
formValues.forEach(function(row) {
var sheetName = row[4]; // the value of "My College Counselor is" column
var sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
var range = sheet.getDataRange();
var data = range.getValues();
var duplicate = false;
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow.join() == row.join()) {
duplicate = true;
break;
}
}
if (!duplicate) {
sheet.appendRow(row);
}
});
}
I'm stuck at this point and am not sure how to proceed.
NOTE: I have code to add a button to the menu list for the Counselors to run this script as needed since the forms can be submitted at any time. Using "onFormSubmit" does not work because there is a potential for multiple students to submit the form at the same time, which I've seen can cause a row or two to not be copied over.
If I understand your question correctly, you want to find a way to avoid duplicated rows, even if you edit them.
In order to do that, you have to define a value for each row that won't change and that is unique. My suggestion would be the following :
Installable trigger with the function custom_onFormSubmit
In the function get Uid (unique ID), and add it to each row submitted
Edit your code in order to search duplicate only with this Uid
First, add this function your Google Form Apps Script:
//add unique ID at a defined column each time a google form is submitted
function custom_onFormSubmit(e){
var uuid = e.triggerUid;
//alternatily you can use:
//var uuid = Utilities.getUuid();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName([SHEETNAME]);
var range = sheet.getDataRange();
var row = range.getLastRow();
sheet.getRange(row, 10).setValue(uuid); //column 10 is for example, adapt to your need
}
------ EDIT: alternative function without trigger onFormSubmit, add this function before
function check_insert_uuid(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName([SHEETNAME]);
var range = sheet.getDataRange();
var values = range.getValues();
for (var x = 0; x < values.length; x++) {
if (values[x][10] == "") {
let uuid = Utilities.getUuid();
range.offset(x, 10, 1, 1).setValue(uuid);
}
}
SpreadsheetApp.flush(); //force new data to sync before copyData
copyData(); //call copy function
}
------ END EDIT -------
Then you just have to edit your function copyData
FROM:
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow.join() == row.join()) {
duplicate = true;
break;
}
}
TO:
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow[10] == row[10]) { //same example of column index 10
duplicate = true;
break;
}
}
References:
Installable Triggers
Google Form Events
Apps Script getuuid (Unique ID are not 100% unique in time and space, but will certainly answer your project)
Based on the help from #waxim-corp, here is the final script that accomplishes my goal:
function onOpen(e) {
let ui = SpreadsheetApp.getUi();
ui.createMenu("🤖 Copy Data 🤖")
.addItem("Let's Do This!", 'checkForID')
.addToUi();
};
function checkForID(){
var ss = SpreadsheetApp.getActive().getSheetByName("Form Responses 1");
var range = ss.getDataRange();
var values = range.getValues();
for (var x = 0; x < values.length; x++) {
if (values[x][0] == "") {
let uuid = Utilities.getUuid();
range.offset(x, 0, 1, 1).setValue(uuid);
}
}
SpreadsheetApp.flush(); //force new data to sync before copyData
copyData(); //call copy function
}
function copyData(){
var formResponses = SpreadsheetApp.getActive().getSheetByName("Form Responses 1");
var formValues = formResponses.getDataRange().getValues();
formValues.shift(); // remove the header row
formValues.forEach(function(row) {
var sheetName = row[5]; // the value of "My College Counselor is" column
var sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
var rangeC = sheet.getDataRange();
var data = rangeC.getValues();
var duplicate = false;
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow[0] == row[0]) {
duplicate = true;
break;
}
}
if (!duplicate) {
sheet.appendRow(row);
}
});
}
I'm sure it could be more efficient, but it works well.

How to clear only "Column A" with Google Apps Script before updating with latest content in the same column

I would like to clear the contents of "column A" before "Update" function fills the latest data in the same column. The idea is remove any redundant data that is not required.
Usecase: Update all the tabs in one Index sheet, if people delete a sheet - that should not reflect in this Index sheet. Here is the code I have used after some research. I am new to this so need some help.
EDIT: Also how to exclude certain "Sheets" from the "Update" function so it doesn't show up in the Index column?
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Index Menu')
.addItem('Create Index', 'createIndex')
.addItem('Update Index', 'updateIndex')
.addToUi();
}
// function to create the index
function createIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
// check if sheet called sheet called already exists
// if no index sheet exists, create one
if (ss.getSheetByName('index') == null) {
var indexSheet = ss.insertSheet('Index',0);
}
// if sheet called index does exist, prompt user for a different name or option to cancel
else {
var indexNewName = Browser.inputBox('The name Index is already being used, please choose a different name:', 'Please choose another name', Browser.Buttons.OK_CANCEL);
if (indexNewName != 'cancel') {
var indexSheet = ss.insertSheet(indexNewName,0);
}
else {
Browser.msgBox('No index sheet created');
}
}
// add sheet title, sheet names and hyperlink formulas
if (indexSheet) {
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
}
// function to update the index, assumes index is the first sheet in the workbook
function updateIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var indexSheet = sheets[0];
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
// function to clear index
function clearContentsOnly() {
var range = SpreadsheetApp
.getActive()
.getSheetByName("Index")
.getRange(4,2,2,2);
range.clearContent();
}
// function to print out the index
function printIndex(sheet,names,formulas) {
sheet.getRange(1,1).setValue('Task Index').setFontWeight('bold');
sheet.getRange(7,1,formulas.length,1).setFormulas(formulas);
}
// function to create array of sheet names and sheet ids
function sheetNamesIds(sheets) {
var indexSheetNames = [];
var indexSheetIds = [];
// create array of sheet names and sheet gids
sheets.forEach(function(sheet){
indexSheetNames.push([sheet.getSheetName()]);
indexSheetIds.push(['=hyperlink("https://docs.google.com/spreadsheets/d/XXXX/edit#gid='
+ sheet.getSheetId()
+ '","'
+ sheet.getSheetName()
+ '")']);
});
return [indexSheetNames, indexSheetIds];
} ```
clear contents of column A
sheet.getRange(1,1,sheet.getLastRow()).clearContent();
if you have header rows and you specify the start row:
const sr = 2;
sheet.getRange( sr, 1, sheet.getLastRow() - sr + 1).clearContent();
function updateIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var indexSheet = sheets[0]; //better to use the name of the index sheet - as the sheet position may get changed easily by the user
indexSheet.GetRange("A2:A").clearContent(); //add this line
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
Make sure you create a Sheet with the name "Index" and then the following code will update column A with all Sheets and directly link to the respective sheet. It will add new sheets with the name and url below existing entries. And if a sheet has changed its name (but not its id), then it will update the sheet name.
function updateIndex(){
const ACTIVE = SpreadsheetApp.getActive()
const spreadsheetURL = ACTIVE.getUrl()
const currentIndexSheet = ACTIVE.getSheetByName("Index")
// Empty the sheet
currentIndexSheet.getRange("A1:A").clearContent()
// We will populate this with all rows
let outputRows = []
// List Sheets which should not be listed
let skipSheets = ["Index", "Another Sheet to Skip"]
// Add Header Row
outputRows.push(['="Linked Sheet"'])
// Get all Sheet and add them to the outputRows using a Hyperlink
ACTIVE.getSheets().forEach( sheet => {
// Skip certain sheets which are defined above
if( skipSheets.indexOf(sheet.getName()) != - 1) return
outputRows.push([`=HYPERLINK("${spreadsheetURL}#gid=${sheet.getSheetId()}", "${sheet.getName()}")`])
})
// Write everything to Index
currentIndexSheet.getRange(1,1, outputRows.length).setFormulas(outputRows)
}

How to automatically merge cells in Google Sheets upon creation of new Sheet

I am trying to figure out a way to make Google Sheets automatically merge Cells A1-C1 when a new sheet is created. My coworker and I have been trying to figure out the script that would make this happen, but everything we have tried only changes the previous Sheet we were working on, not the new one.
So far these are the two scripts we have tried, just to get some sort of result we are looking for:
function formatCells() {
var ss = SpreadsheetApp.getActiveSpreadsheet ();
var s = ss.getSheetByName('Combined')
var range = s.getDataRange()
var values = range.getValues();
for( var row = values.length -1; row >= 0; --row)
if (values[row][1] == 'Hello')
{s.getRange(row+1,1).mergeAcross();
}
}
and
function newSheetTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('newSheet')
.forSpreadsheet(ss)
.onChange()
.create();
}
function newSheet(e){
if (e.changeType == 'INSERT_GRID') {
SpreadsheetApp.flush();
SpreadsheetApp.getActiveSheet().getRange('A1:C1').merge();
}
}
Does anyone have an idea of where we went wrong?
The problem is that theonChange trigger is not able to detect the active sheet correctly
Retrieving the active sheet on trigger will always return you the first sheet, as you can easily verify with
function myFunction(e) {
Logger.log(e.changeType);
if(e.changeType=="INSERT_GRID"){
Logger.log(SpreadsheetApp.getActive().getActiveSheet().getName());
}
}
So you need to implement a workaround.
For example:
Strore the present sheet names in Script properties
When the trigger fires and the condition e.changeType=="INSERT_GRID" is fullfilled:
Compare the currently present sheet number to the one stored in script properties to evaluate either a new sheet has been inserted
If the sheet number increased - find the name of the new sheet with indexOf()
Merge cells on the new sheet and update the script properties
Code snippet:
var ss = SpreadsheetApp.getActive();
//run me once
function firstSetUp(){
var sheets = ss.getSheets();
var names = [];
for (var i = 0; i < sheets.length; i++){
names.push(sheets[i].getName())
}
PropertiesService.getScriptProperties().setProperty("sheets", JSON.stringify(names) );
}
//run me on trigger
function newSheet(e) {
if(e.changeType=="INSERT_GRID"){
var newSheets = ss.getSheets();
var oldSheetNames = JSON.parse(PropertiesService.getScriptProperties().getProperty("sheets"));
Logger.log(oldSheetNames);
var length = oldSheetNames.length;
Logger.log("length : " + length);
if (length != newSheets.length){
for (var i = 0; i < newSheets.length; i++){
if(oldSheetNames.indexOf(newSheets[i].getName()) == -1){
var newSheet = newSheets[i];
Logger.log(newSheet.getName());
newSheet.getRange('A1:C1').merge();
oldSheetNames.push(newSheet.getName());
PropertiesService.getScriptProperties().setProperty("sheets", JSON.stringify(oldSheetNames));
break;
}
}
}
}
}

ImportRange: Error: Cannot find range or sheet for imported range

Google Spreadsheet.
I dynamically remove old sheet and I create (I use sheet.copyTo() function) a new sheet with the same name, but IMPORTRANGE formula can't find (periodically) new Sheets.
=ImportRange("ssId", "Data!A1:B1")
This script works fine but other Spreadsheets which fetch data from this Spreadsheet by IMPORTRANGE formula periodically can't find new (generated) sheets:
=QUERY(IFERROR(IMPORTRANGE("ssId", "Data!A:AU")),"SELECT * ", 0)
This formula doesn't return anything!
function copySpreadSheet(sourceId, targetId) {
try {
var sourceSS = SpreadsheetApp.openById(sourceId);
SpreadsheetApp.setActiveSpreadsheet(sourceSS);
var sourceSheets = sourceSS.getSheets();
var targetSS = SpreadsheetApp.openById(targetId);
SpreadsheetApp.setActiveSpreadsheet(targetSS);
var targetSheets = targetSS.getSheets();
//you can't delete all sheets!
var timestamp = new Date().getTime();
var tempName = "TEMPORARY-FOR-DELETE-" + timestamp;
targetSS.insertSheet(tempName);
for(var i = 0; i < targetSheets.length; i++) {
targetSS.deleteSheet(targetSheets[i]);
}
var sourceCharts = [];
var newSheetCharts = [];
var newSheet = null;
var newSheetName = "";
for(var i = 0; i < sourceSheets.length; i++) {
newSheet = sourceSheets[i].copyTo(targetSS)
newSheetName = newSheet.getName().replace("Copy of ", "");
newSheet.setName(newSheetName);
/*
newSheetCharts = newSheet.getCharts();
sourceCharts = sourceSheets[i].getCharts();
for(var j = 0; j < sourceCharts.length; j++) {
/* Server Error:
var chart = newSheetCharts[j].modify()
.setChartType(sourceCharts[j].getType())
.addRange(sourceCharts[j].getRange())
.build();
newSheet.updateChart(chart);
*/
//insertImage(blob, column, row, offsetX, offsetY)
/*
newSheet.insertImage(
sourceCharts[j].getBlob(),
sourceCharts[j].getContainerInfo().getAnchorColumn(),
sourceCharts[j].getContainerInfo().getAnchorRow(),
sourceCharts[j].getContainerInfo().getOffsetX(),
sourceCharts[j].getContainerInfo().getOffsetY()
);
*/
/* } */
}
targetSS.deleteSheet(targetSS.getSheetByName(tempName)); //remove a temporary sheet
//Adding Date Stamp:
targetSS.insertSheet("Last time updated").hideSheet().getRange(1, 1).setValue(new Date());
SpreadsheetApp.flush();
Utilities.sleep(500);
return targetSS;
} catch (err) {
Logger.log(err.toString());
}
}
I tried to "cheat" the Spreadsheet:
var querySheet = dataSourceSS.getSheetByName("Query");
querySheet.activate();
Logger.log(formula);
querySheet.getRange(2, 1).setFormula("=Minute(Now())"); //any FORMULA
Utilities.sleep(1000);
querySheet.getRange(2, 1).setFormula(formula);
It still doesn't work!
Creating a new sheet with the same name is no guarantee that it will be treated as a replacement for the old sheet in all circumstances. Sheets have ID numbers that are used to identify them independently of names. If the importrange has been linked to a particular sheet, and that sheet is deleted, there is no promise that it will automatically re-link to another sheet that has the same name. Possible solutions:
Delete and re-enter the importrange formulas; this can also be done with a script if it has access to those spreadsheets
(Preferable): do not delete and re-created sheets. Clear the existing sheet with and copy new values to it.
Example:
targetSheets[i].clear()
var values = sourceSheets[i].getDataRange().getValues();
targetSheets[i].getRange(1, 1, values.length, values[0].length).setValues(values);
This will not copy formatting or formulas; the effect is essentially same as copying and pasting values only. But importrange gets only the values, so it makes no difference to it.

Google script duplicate and rename sheets based on list

I have a table that has three columns of employee info. The first column has the employe names. I want to write a google apps script that will duplicate a pre-formatted template sheet and re-name it with the employee name. At the end of the script each employee will have their own sheet named after them.
Here is the code I have so far, I am using some functions from the Google scripts tutorial, but I am at a loss on how to proceed further. EDITED, I have gotten a little further, this code worked once but now is getting hung on setName:
//Create new sheets for each employee in the list
function createEmployeeSheets() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
// Get the range of cells that store employee data.
var employeeDataRange = ss.getRangeByName("EmployeeRef");
// For every row of employee data, generate an employee object.
var employeeObjects = getRowsData(sheet, employeeDataRange);
for (i=0; i < employeeObjects.length; i++) {
var EmployeeName = employeeObjects[i].name;
ss.setActiveSheet(ss.getSheetByName("Template"));
SpreadsheetApp.getActiveSpreadsheet().duplicateActiveSheet();
var first = ss.getSheetByName("Copy of Template 1");
first.setName(EmployeeName);
}
}
After visiting this Q&A I figured out a far simpler method:
function createEmployeeSheets() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Get the range of cells that store employee data.
var employeeDataRange = ss.getRangeByName("EmployeeRef");
var employeeObjects = employeeDataRange.getValues();
var template = ss.getSheetByName('Template');
for (var i=0; i < employeeObjects.length; i++) {
// Put the sheet you want to create in a variable
var sheet = ss.getSheetByName(employeeObjects[i]);
// Check if the sheet you want to create already exists. If so,
// log this and loop back. If not, create the new sheet.
if (sheet) {
Logger.log("Sheet " + employeeObjects[i] + "already exists");
} else {
template.copyTo(ss).setName(employeeObjects[i]);
}
}
return;
}
You can do more simple by using the copyTo() function.
Also make sure you have unique EmployeeNames.
So your code would look like:
function test() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var employeeObjects = [
{"name": "Peter" },
{"name": "Alice" },
{"name": "Frank" }
]
var template = ss.getSheetByName('Template');
for ( var i=0; i < employeeObjects.length; i++) {
var EmployeeName = employeeObjects[i].name;
// get the sheets to check you are not creating a duplicate sheet
var sheets = ss.getSheets();
var ok = true;
// loop through the sheets and check a duplicate exist
for ( var j=0; j<sheets.length;j++ ) {
if ( sheets[j].getName() == EmployeeName ) {
ok = false;
Logger.log('duplicate');
}
}
if ( ok ) {
template.copyTo(ss).setName(EmployeeName);
} else {
// do whatever you need to do if employee name is duplicate
}
}
}