Is it possible to batch process range protections in Google Apps Script? - optimization

I have to create a dozen protected ranges in a sheet. I have code that works but is very slow because it contacts the server for each range. I know it's possible to work on a local copy of the data if there's some cell processing involved. Is it possible for range protections also?
If it's not, would caching help?
The below code uses the username from the first row as an editor for a bunch of rows in the same column.
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var sheets = spreadSheet.getSheets();
//Set protections per column, we start from the 4th.
for (var i = 4; i <= sheets[3].getLastColumn(); i++){
///Get the username.
var editor = sheets[3].getRange(1, i).getDisplayValue();
//Set the protection.
var protection = sheets[3].getRange(3, i, 22, 1).protect();
protection.setDescription(editor);
//Handle the case of deleted/unknown usernames.
try{
protection.addEditor(editor + '#domain.com');
} catch(error){
protection.addEditor('user#domain.com');
}
}
I've found a solution for a similar issue https://stackoverflow.com/a/37820854 but when I try to apply it to my case I get an error "TypeError: Cannot find function getRange in object Range" so I must be doing something wrong.
var test = [];
for (var i = 4; i <= sheets[3].getLastColumn(); i++){
test.push(sheets[3].getRange(3, i, 22, 1));
}
var editor;
for (var i = 0; i<test.length; i++){
var editor = test[i].getRange(1, 1).getDisplayValue();
}

The syntax for the method getRange() is getRange(row, column, numRows, numColumns), while you counter variable i loops through the COLUMNS instead of ROWS.
If your intention is to loop through all columns and add an editor to each one, it should be something like
for (var i = 4; i <= sheets[3].getLastColumn(); i++){
///Get the username.
var editor = sheets[3].getRange(1, i).getDisplayValue();
//Set the protection.
var protection = sheets[3].getRange(startRow, i, rowNumber, columnNumber).protect();
protection.setDescription(editor);
//Handle the case of deleted/unknown usernames.
try{
protection.addEditor(editor + '#domain.com');
} catch(error){
protection.addEditor('user#domain.com');
}
}

Its possible to do batch processing.
But you'll have to use Advanced Google Services. Check out the Sheets Advanced service and the Sheets API documentation.

Related

Merging many spreadsheets into report file exceeds maximum execution time

I am using the following script to add rows of files from a student loop in the Google spreadsheet if credits are less than x. The script was working good but as the data in the spreadsheet is being added daily, now the script is throwing "Exceeded maximum execution time" error (we have more than 2000 files). As I am new to scripting I don't know how to optimize the code.
Could someone help me to optimize the code or any solution so that the execution time take less than 5 min. Every time you compare to an email, it has to be compared to many emails. Please Help!
function updated() {
//Final file data (Combined)
var filecombined = SpreadsheetApp.openById("XXXXXXXXXX");
var sheet2 = filecombined.getSheets();
//Folder with all the files
var parentFolder = DriveApp.getFolderById("YYYYYYYYYYYY");
var files = parentFolder.getFiles();
//Current Date
var fecha = new Date();
//Path for each file in the folder
while (files.hasNext()) {
var idarchivo = files.next().getId();
var sps = SpreadsheetApp.openById(idarchivo);
var sheet = sps.getSheetByName('STUDENT PROFILE');
var data = sheet.getDataRange().getValues();
var credits = data[5][1];
//Flat; bandera:1 (new row), bandera:2 (update row)
var bandera = 1;
//Take data from final file (Combined)
var data2 = sheet2[0].getDataRange().getValues();
//If credits are less than X: write
if (credits < 120) {
var email = data[2][1];
var lastrow = filecombined.getLastRow();
var u = 0;
//comparison loop by email, if found it, update and exit the loop
while (u < lastrow) {
u = u + 1;
if (email == data2[u - 1][1]) {
sheet2[0].getRange(u, 3).setValue(credits);
sheet2[0].getRange(u, 4).setValue(fecha);
u = lastrow;
bandera = 2;
}
}
//if that email does not exist, write a new row
if (bandera == 1) {
var nombre = data[0][1];
sheet2[0].getRange(lastrow + 1, 1).setValue(nombre);
sheet2[0].getRange(lastrow + 1, 2).setValue(email);
sheet2[0].getRange(lastrow + 1, 3).setValue(credits);
sheet2[0].getRange(lastrow + 1, 4).setValue(fecha);
}
}
}
SpreadsheetApp.flush();
}
The questioner's code is taking taking more than 4-6 minutes to run and is getting an error Exceeded maximum execution time.
The following answer is based solely on the code provided by the questioner. We don't have any information about the 'filecombined' spreadsheet, its size and triggers. We are also in the dark about the various student spreadsheets, their size, etc, except that we know that there are 2,000 of these files. We don't know how often this routine is run, nor how many students have credits <120.
getvalues and setvalues statements are very costly; typically 0.2 seconds each. The questioners code includes a variety of such statements - some are unavoidable but others are not.
In looking at optimising this code, I made two major changes.
1 - I moved line 27 var data2 = sheet2[0].getDataRange().getValues();
This line need only be executed once and I relocated it at the top of the code just after the various "filecombined" commands. As it stood, this line was being executed once for every student spreadsheet; this along may have contributed to several minutes of execution time.
2) I converted certain setvalue commands to an array, and then updated the "filecombined" spreadsheet from the array once only, at the end of the processing. Depending on the number of students with low credits and who are not already on the "filecombined" sheet, this may represent a substantial saving.
The code affected was lines 47 to 50.
line47: sheet2[0].getRange(lastrow+1, 1).setValue(nombre);
line48: sheet2[0].getRange(lastrow+1, 2).setValue(email);
line49: sheet2[0].getRange(lastrow+1, 3).setValue(credits);
line50: sheet2[0].getRange(lastrow+1, 4).setValue(fecha);
There are setvalue commands also executed at lines 38 and 39 (if the student is already on the "filecombined" spreadsheet), but I chose to leave these as-is. As noted above, we don't know how many such students there might be, and the cost of these setvalue commands may be minor or not. Until this is clear, and in the light of other time savings, I chose to leave them as-is.
function updated() {
//Final file data (Combined)
var filecombined = SpreadsheetApp.openById("XXXXXXXXXX");
var sheet2 = filecombined.getSheets();
//Take data from final file (Combined)
var data2 = sheet2[0].getDataRange().getValues();
// create some arrays
var Newdataarray = [];
var Masterarray = [];
//Folder with all the files
var parentFolder = DriveApp.getFolderById("YYYYYYYYYYYY");
var files = parentFolder.getFiles();
//Current Date
var fecha = new Date();
//Path for each file in the folder
while (files.hasNext()) {
var idarchivo = files.next().getId();
var sps = SpreadsheetApp.openById(idarchivo);
var sheet = sps.getSheetByName('STUDENT PROFILE');
var data = sheet.getDataRange().getValues();
var credits = data[5][1];
//Flat; bandera:1 (new row), bandera:2 (update row)
var bandera = 1;
//If credits are less than X: write
if (credits < 120){
var email = data[2][1];
var lastrow = filecombined.getLastRow();
var u = 0;
//comparison loop by email, if found it, update and exit the loop
while (u < lastrow) {
u = u + 1;
if (email == data2[u-1][1]){
sheet2[0].getRange(u, 3).setValue(credits);
sheet2[0].getRange(u, 4).setValue(fecha);
u = lastrow;
bandera = 2;
}
}
//if that email does not exist, write a new row
if(bandera == 1){
var nombre = data[0][1];
Newdataarray = [];
Newdataarray.push(nombre);
Newdataarray.push(email);
Newdataarray.push(credits);
Newdataarray.push(fecha);
Masterarray.push(Newdataarray);
}
}
}
// update the target sheet with the contents of the array
// these are all adding new rows
lastrow = filecombined.getLastRow();
sheet2[0].getRange(lastrow+1, 1, Masterarray.length, 4);
sheet2[0].setValues(Masterarray);
SpreadsheetApp.flush();
}
As I mentioned in my comment, the biggest issue you have is that you repeatedly search an array for a value, when you could use a much faster lookup function.
// Create an object that maps an email address to the (last) array
// index of that email in the `data2` array.
const knownEmails = data2.reduce(function (acc, row, index) {
var email = row[1]; // email is the 2nd element of the inner array (Column B on a spreadsheet)
acc[email] = index;
return acc;
}, {});
Then you can determine if an email existed in data2 by trying to obtain the value for it:
// Get this email's index in `data2`:
var index = knownEmails[email];
if (index === undefined) {
// This is a new email we didn't know about before
...
} else {
// This is an email we knew about already.
var u = ++index; // Convert the array index into a worksheet row (assumes `data2` is from a range that started at Row 1)
...
}
To understand how we are constructing knownEmails from data2, you may find the documentation on Array#reduce helpful.

EPPlus - How to set data type of a column to General

In my ASP.NET Core 1.1 app I'm using EPPlus.Core to export data to Excel. Some columns exported have mostly numbers but rarely a text (e.g. N/A). These columns in generated Excel are showing (as expected) a green triangle on the top left corner of their cells. I want to get rid of those warnings.
Question: What's a good way of getting rid of those triangles when Excel is generated? I tried setting the format of these columns to text as follows but it did not work. I guess we need to set the format of these columns to General but I can't figure out how:
workSheet.Cells["D1:P1"].Style.Numberformat.Format = "General";
UPDATE:
Per a user's request, the code looks similar to following.
Error at inner loop for (var j = 4; j < testlist[i].Count(); j++){...}: MyViewModel does not contain a definition of Count()
Error at line if (testlist[i][j] is string){...}: cannot apply indexing with [] to an extension of type MyViewModel
Controller:
....
....
var testlist = (qry to load a MyViewModel).ToList();
using (ExcelPackage pkg= new ExcelPackage())
{
var ws = excel.Workbook.Worksheets.Add("TestWorkSheet");
ws.Cells[1, 1].LoadFromCollection(rc_excel, true);
//I'm starting from 2nd row and 5th column
for (var i = 1; i < testlist.Count; i++)
{
for (var j = 4; j < testlist[i].Count(); j++)
{
if (testlist[i][j] is string)
{
....
....
}
}
pkg.Save();
return(....);
}

How to send specific rows of excel to data provider in testNG?

I have created an excel containing multiple rows, each row corresponding to a test case. The Excel has around 22 columns (parameters) including a "Flag" column.
I want the Dataprovider to return only those columns which has a value 'Y' in the excel column. The use case is that when the client wants to run a particular test case, they only need to flag it to Y or N. How can I achieve this is in TestNG with Selenium?
My colleague had helped me to achieve this using following code, but this does not work as per my new code structure.
#DataProvider(name = "Order")
public Object[][] menu()
{
Object[][] data = UtilLibrary.getData("Order");
int intColCount = UtilLibrary.datatable.getColumnCount("Order");
int j = 0;
int arrRowCount=0;
for (int i = 0; i < data.length; i++) {
if((data[i][intColCount-1]).equals("Y"))
{
arrRowCount++;
}
}
j=0;
Object[][] retData = new Object[arrRowCount][intColCount];
for (int i = 0; i < data.length; i++)
{
if ((data[i][intColCount-1]).equals("Y")) {
retData[j] = data[i]; j++; }
}
return retData;
}
The above code sends only the record/s having flag='Y' in the excel to the Dataprovider. But, it was working only when the test script had a single #Test method having all automation steps, while now I have multiple #Test methods to simulate the same steps to which I have passed this same Dataprovider (Order).
Let me know if someone has achieved this using a similar code or if TestNG has a specific feature to send filtered rows of excel to the Dataprovider
I have now fixed the issue. The reason why the above given code for rows having flag = 'Y' in Excel was not working with multiple #Test methods was only due to a silly mistake. The mistake was that I had changed the excel structure a bit and did not modify the above code as per the index of the new flag column.

Installable Trigger Failing with Test Add-On

I have been wrestling with an installable trigger issue for a couple of days now. All of my research indicates that an add-on should allow for an installable onEdit() trigger within a spreadsheet, but my attempts keep erroring out. I have simplified my project code a bit to exemplify my issue.
The error message:
Execution failed: Test add-on attempted to perform an action that is not allowed.
My code (listing functions is the order that they are called):
function onOpen() //creates custom menu for the evaluation tool ***FOR ADMININSTRATORS ONLY***
{
var ui = SpreadsheetApp.getUi();
if(!PropertiesService.getDocumentProperties().getProperty('initialized'))
{
ui.createMenu('Evaluation Menu') // Menu Title
.addItem('Create Installable OnEdit Trigger', 'createInstallableOnEditTrigger')
.addToUi();
}
else
{
ui.createMenu('Evaluation Menu') // Menu Title
.addSubMenu(ui.createMenu('Manage Observations & Evidence')
.addSubMenu(ui.createMenu('Create New Observation')
.addItem('Formal', 'createNewFormalObservation')
.addItem('Informal', 'createNewInformalObservation')
)
.addToUi();
}
}
function createInstallableOnEditTrigger() { // installable trigger to create employee look-up listener when user edits the EIN fields on the Documentation Sheet.
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('onEditListener')
.forSpreadsheet(ss)
.onOpen()
.create();
PropertiesService.getDocumentProperties().setProperty('initialized','true');
}
function onEditListener(event) //this function conitnually listens to all edit, but only engages only certain conditions such as when a timestamp is determined to be needed or the Documentation Sheet needs to be auto-populated
{
//Determine whether or not the conditions are correct for continuing this function
var sheetName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName(); //determines the name of the currently active sheet
if (sheetName.indexOf("Evidence") > -1) // if the active sheet is an evidence collection sheet, a timestamp may be needed
{
populateEvidenceTimeStamp(event, sheetName);
}
else if (sheetName == "Documentation Sheet") //if the active sheet is the "Documentation Sheet" than auto-population and EIN lookups may be needed
{
employeeLookup(event, sheetName);
}
}
What am I missing? Any help is greatly appreciated!!
The below code has been added as requested by #Mogsdad.
populateEvidenceTimeStamp() is dependent upon generateTimeStamp() which is also included below:
function populateEvidenceTimeStamp(event, sheetName)
{
var evidenceColumnName = "Evidence";
var timeStampColumnName = "Timestamp";
var sheet = event.source.getSheetByName(sheetName);
var actRng = event.source.getActiveRange();
var indexOfColumnBeingEdited = actRng.getColumn();
var indexOfRowBeingEdited = actRng.getRowIndex();
var columnHeadersArr = sheet.getRange(3, 1, 1, sheet.getLastColumn()).getValues(); // grabs the column headers found in the 3rd row of the evidence sheet
var timeStampColumnIndex = columnHeadersArr[0].indexOf(timeStampColumnName); //determines the index of the Timestamp column based on its title
var evidenceColumnIndex = columnHeadersArr[0].indexOf(evidenceColumnName); evidenceColumnIndex = evidenceColumnIndex+1; //determines the index of the evidence column based on its title
var cell = sheet.getRange(indexOfRowBeingEdited, timeStampColumnIndex + 1); //determines the individual timestap cell that will be updated
if (timeStampColumnIndex > -1 && indexOfRowBeingEdited > 3 && indexOfColumnBeingEdited == evidenceColumnIndex && cell.getValue() == "") // only create a timestamp if 1) the timeStampColumn exists, 2) you are not actually editing the row containing the column headers and 3) there isn't already a timestamp in the Timestamp column for that row
{
cell.setValue(generateTimeStamp());
}
}
function generateTimeStamp()
{
var timezone = "GMT-7"; // Arizona's time zone
var timestamp_format = "MM.dd.yyyy hh:mm:ss a"; // timestamp format based on the Java SE SimpleDateFormat class. http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
var currTimeStamp = Utilities.formatDate(new Date(), timezone, timestamp_format);
return currTimeStamp;
}
Below is the employeeLookup() function which is dependent upon lookupEIN()
function employeeLookup(event, sheetName)
{
if(sheetName == "Documentation Sheet" && !PropertiesService.getDocumentProperties().getProperty('initialized')) // if the activeSheet is "Documentation Sheet" and the sheet has not yet been initialized
{
var actRng = event.source.getActiveRange();
Logger.log("employeeLookup(): actRng: "+actRng.getRow()+" , "+actRng.getColumn());
if(actRng.getRow() == 4 && actRng.getColumn() == 9 && event.source.getActiveRange().getValue() != "") //if the "Teacher EIN" cell is the active range and it's not empty
{
var ein = actRng.getValue();
clearDocumentationSheetTeacherProfile(); //first clear the teacher profile information to avoid the possibility of EIN/Teacher Info mismatch if previous search did not yield results
var teacherDataArr = lookupEIN(ein, "Teachers");
if(teacherDataArr)
{
//write retrieved teacher data to Documentation Spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Documentation Sheet");
sheet.getRange(5, 9, 1, 1).setValue(teacherDataArr[1]); // Teacher First Name
sheet.getRange(6, 9, 1, 1).setValue(teacherDataArr[2]); // Teacher Last Name
sheet.getRange(7, 9, 1, 1).setValue(teacherDataArr[3]); // Teacher Email
sheet.getRange(11, 9, 1, 1).setValue(teacherDataArr[4]); // School Name
sheet.getRange(11, 39, 1, 1).setValue(teacherDataArr[5]); // Site Code
sheet.getRange(10, 30, 1, 1).setValue(calculateSchoolYear()); //School Year
}
else
{
Logger.log("employeeLookup(): type:Teachers 'died. lookupEIN() did not return a valid array'"); //alert message already sent by lookUpEIN
}
}
else if (actRng.getRow() == 4 && actRng.getColumn() == 30 && actRng.getValue() != "" && !PropertiesService.getDocumentProperties().getProperty('initialized')) //if the "Observer EIN" cell is the active range
{
Logger.log("employeeLookup(): 'active range is Observer EIN'");
var ein = actRng.getValue();
clearDocumentationSheetObserverProfile(); //first clear the teacher profile information to avoid the possibility of EIN/Observer Info mismatch if previous search did not yield results
var observerDataArr = lookupEIN(ein, "Observers");
if(observerDataArr)
{
//write retrieved observer data to Documentation Spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Documentation Sheet");
sheet.getRange(5, 30, 1, 1).setValue(observerDataArr[1]); // Observer First Name
sheet.getRange(6, 30, 1, 1).setValue(observerDataArr[2]); // Observer Last Name
sheet.getRange(7, 30, 1, 1).setValue(observerDataArr[3]); // Observer Email
}
else
{
Logger.log("employeeLookup(): type:Observers 'died. lookupEIN() did not return a valid array'"); //alert message already sent by lookUpEIN
}
}
else
{
Logger.log("employeeLookup(): 'active range is not a trigger'");
//do nothing (not the right cell)
}
}
else
{
//Observer log has already been initialized and documentation cannot be altered. notify user
Logger.log("employeeLookup(): 'log already saved.... alerting user'");
logAlreadyInitializedDialogue();
restoreDocumentationSheetData();
}
}
function lookupEIN(ein, type)
{
Logger.log ("lookUpEIN(): 'engaged'");
var ss = SpreadsheetApp.openById(teacherObserverIndex_GID);
var sheet = ss.getSheetByName(type); //lookup type aligns with the individual sheet names on the teacherObserverIndex_GID document
var values = sheet.getDataRange().getValues();
var val = sheet.getDataRange();
for (var i = 1; i < values.length; i++)
{
if(values[i][0] == ein)
{
Logger.log ("lookUpEIN(): values[i]: "+values[i]);
return values[i];
}
else
{
Logger.log ("lookUpEIN(): 'no match found'");
}
}
//a match could not be found
Logger.log("An EIN match could not be found"); // create a feedback pop-up
einNotFoundDialogue(type); //alert user that there is a problem with the provided ein
}
Triggers can't be created when running a script as Test as add-on.
From https://developers.google.com/apps-script/add-ons/test :
There are a number of things to keep in mind while testing add-ons:
Installable triggers are currently not supported when testing.
Functionality that depends on installable triggers will not be
testable.
Some possible workarounds
For on open and on edit installable triggers, temporally add simple triggers to call the functions of the installable triggers. This might only work if the execution time of is less than the simple triggers limit.
Call the functions from the installable triggers from functions that create object that emulates the corresponding event object
Instead of using a stand-alone project use bounded projects. You might use CLASP or an extension like Google Apps Script GitHub Assistant Chrome extension to make it easier to copy the code from the stand-alone project to a bounded project.
Related
How can I test a trigger function in GAS?
In my experience onEdit() is not available for test as Add-On.
I agree the documentation is not clear, it seems to be referring to only "Installable Triggers" but I think it applies to all Triggers except for the "onInstall" trigger that is run as soon as you start the test. (see: Testing Google Sheet Addon Triggers for more details)

Refresh a dijit.form.Select

First, you have to know that I am developing my project with Struts (J2EE
Here is my problem :
I have 2 dijit.form.Select widgets in my page, and those Select are filled with the same list (returned by a Java class).
When I select an option in my 1st "Select widget", I would like to update my 2nd Select widget, and disable the selected options from my 1st widget (to prevent users to select the same item twice).
I succeed doing this (I'll show you my code later), but my problem is that when I open my 2nd list, even once, it will never be refreshed again. So I can play a long time with my 1st Select, and choose many other options, the only option disabled in my 2nd list is the first I've selected.
Here is my JS Code :
function removeSelectedOption(){
var list1 = dijit.byId("codeModif1");
var list2 = dijit.byId("codeModif2");
var list1SelectedOptionValue = list1.get("value");
if(list1SelectedOptionValue!= null){
list2.reset();
for(var i = 0; i < myListSize; i++){
// If the value of the current option = my selected option from list1
if(liste2.getOptions(i).value == list1SelectedOptionValue){
list2.getOptions(i).disabled = true;
} else {
list2.getOptions(i).disabled = false;
}
}
}
Thanks for your help
Regards
I think you have to reset() the Select after you've updated its options' properties. Something like:
function removeSelectedOption(value)
{
var list2 = dijit.byId("codeModif2"),
prev = list2.get('value');
for(var i = 0; i < myListSize; i++)
{
var opt = myList[i];
opt.disabled = opt.value === value;
list2.updateOption(opt);
}
list2.reset();
// Set selection again, unless it was the newly disabled one.
if(prev !== value) list2.set('value', prev);
};
(I'm assuming you have a myList containing the possible options here, and the accompanying myListSize.)