I have a single worksheet that contains user entered responses in Columns C & D, Rows 3 - 20. The responses are time dependent and look at Column E Rows 3-20 to see if it is "Locked" or "Open".
Using protection, I lock the entire sheet for editing with the exception of C3:D20. The sheet is set to calculate every minute.
I am trying to write a script that checks the column E to see if it is set for locked or open. If it is set for locked, I would like to lock (protect) columns C&D in that row for editing from everyone but myself. I run the script every 5 minutes and I have the for loop and if statement handled, but when I go to use the RemoveEditors function it does 2 things:
Creates a new protected range (so after 5 minutes I have 1 additional protected range, 10 minutes, I have 2 additional, etc.)
Does not remove the other editors from those able to edit the cells.
I tried using Google's example code, but their code adds the current user as an editor, which is what I'm trying to avoid doing since then that editor can just remove the protection that the code is putting in place.
Any help you could provide would be appreciated.
Current Code is below:
function Lock_Cells() {
var sheet = SpreadsheetApp.getActive();
for (var i = 3; i <= 20; i++)
{
var Check_Cell = "E" + i;
var Temp = sheet.getRange(Check_Cell).getValue();
if (Temp == "Locked")
{
var Lock_Range = "C" + (i + 2) + ":D" + "i";
var protection = sheet.getRange(Lock_Range).protect();
var description = "Row " + i;
protection.setDescription(description);
var eds = protection.getEditors();
protection.removeEditors(eds);
}
}
}
To avoid creating a new set of protected ranges, you can add logic to check which rows are already locked. With that information you just need to skip those rows:
note: there was a mistake in this line: var Lock_Range = "C" + (i + 2) + ":D" + "i"; the variable i should not have quotation.
function Lock_Cells() {
var sheet = SpreadsheetApp.getActive();
var rows = get_protected_Rows();
for (var i =3; i <= 20; i++)
{
var Check_Cell = "E" + i;
var cell = sheet.getRange(Check_Cell);
var Temp = sheet.getRange(Check_Cell).getValue();
if (Temp == "Locked" && rows.indexOf(i) <0)
{
var Lock_Range = "C" + i + ":D" + i; //In this line you put "i"
.....
...
}
function get_protected_Rows()
{
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
var rows = [];
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
var anotation = protection.getRange().getRow();
rows.push(anotation);
}
return rows
}
You are right, when the code is executed by one of the users, the protection gives that user the ability to edit those rows. I would recommend that as the owner of the file, you also run a task to remove every other editor from those rows. The function would be very similar to the previous. And I know is not the best but it may help you with your use case.
function remove_editors()
{
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
var anotation = protection.getRange().getA1Notation();
var eds = protection.getEditors();
protection.removeEditors(eds);
}
}
By doing that i was able to restrict the permission to other users. Hope it helps.
Related
I'm trying to create an apps script to autofill a gantt chart when the sheet is edited, but having trouble.
Here is a link to the spreadsheet if it helps.
function ganttChart()
{
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ganttSheet = ss.getSheetByName("Gantt Chart");
var headerRow = ss.ganttSheet.getRange('headerRow').getRow();
var lastRow = ss.ganttSheet.getLastRow();
var lastCol = ss.ganttSheet.getLastColumn();
var firstTask = headerRow + 1
var taskRoleCol = ss.ganttSheet.getRange('taskRole').getColumn();
//I'm not sure if I need to do the below RoleCol if I already have a named range -- this will return an integer which is the column #
var roleCol = ss.getSheetByName("Roles").getRange('Roles').getColumn();
var taskCol = ss.ganttSheet.getRange('taskNames').getColumn();
var startWeekRow = ss.ganttSheet.getRange('startWeek').getRow();
var expDurationCol = ss.ganttSheet.getRange('expDuration').getColumn();
//set the requirements for the edit trigger -- not sure what these would be
//if (e.range)
//{
for (var i = firstTask; i < lastRow; i++)
{
var currentTask = ss.ganttSheet.getRange(i, taskCol).getValue();
var currentStartWeek = ss.ganttSheet.getRange(i, startWeekCol).getValue();
var currentTaskExpDuration = ss.ganttSheet.getRange(i,expDurationCol).getValue();
var currentTaskRole = ss.ganttSheet.getRange(i,taskRoleCol).getValue();
if (currentTask != null)
{
if (currentStartWeek != null)
{
//for loop to identify the column that matches the start week #
for (var j = 0; j < lastCol; j++)
{
var checkWeek = ss.ganttSheet.getRange(startWeekRow, j).getValue();
if (checkWeek == currentStartWeek)
{
//identify the range
var taskTimeRange = ss.ganttSheet.getRange(i,j - 1,(currentTaskExpDuration*2 +1), 1);
//for loop get the background color based on role
for (var k = 0; k < lastRow; k++)
{
var checkRole = ss.ganttSheet.getRange(k, roleCol).getValue();
//if role value matches the currentTaskRole
if (checkRole == currentTaskRole)
{
var roleColor = ss.ganttSheet.getRange(k, roleCol).getBackground();
//reformat the range based on duration
taskTimeRange.setBackground(roleColor);
}
}
}
}
}
}
}
//}
}
I took off the "onEdit" to try and get the program to work on run, but I'm still getting a "cannot read properties of undefined" error.
What should happen is:
when a user edits the "Gantt Chart" sheet
the program changes the background color in the corresponding range to indicate the weeks a task is being worked on, based on the start week and calculated duration
the background color should correspond to the task role, based on the colors set in the "Roles" sheet
If the above isn't clear, here is a link to a video where I try to explain what the program should do
The question relates to onEdit but the trigger is, at this point of development of the script, irrelevant, since the script is littered with syntax errors.
Even so, I suggest that the trigger is irrelevant in any event. The script can/should be triggered when the data has been populated. This trigger can be done manually, or (perhaps) via a menu option.
getRange(), getLastRow() and getLastColumn are sheet-based methods.
Incorrect
var headerRow = ss.ganttSheet.getRange('headerRow').getRow()
var lastRow = ss.ganttSheet.getLastRow()
var lastCol = ss.ganttSheet.getLastColumn()
Correct
var headerRow = ganttSheet.getRange('headerRow').getRow()
var lastRow = ganttSheet.getLastRow()
var lastCol = ganttSheet.getLastColumn()
Other
startWeekCol is not defined
for (var j = 0; j < lastCol; j++)
"j" substitutes for the column number, but a value of 0 is invalid
var taskTimeRange = ganttSheet.getRange(i,j - 1,(currentTaskExpDuration*2 +1), 1)
when "j" is 1, "j-1" resolves to 0 (zero) which is invalid
for (var k = 0; k < lastRow; k++)
"k" substitutes for the row number, but a value of 0 is invalid
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.
As an alternative to using an Arrayformula, I have the following script which basically auto-increment
cells within a specific column to generate a unique ID
The only problem that I'm facing, is that if 2 users tick the checkbox at the same exact time (which I've tried myself even with half second delay) it gives them the same ID, which is a problem for the next task.
Is there any trick (or alternative approach) to get around this little problem? At the moment I'm just using a conditional formatting to highlight any duplicates of Ids to help users (i.e. if a user sees the ID in red, he deletes it, then tick the checkbox again to get a new ID)
Thank you.
SCRIPT:
function onEdit(e) {
var watchColumns = [1]; //when checkbox is ticked, auto-numbering will be triggered
var autoColumn = 3;
var headerRows = 1;
var watchSheet = "Sheet1";
var range = e.range;
var sheet = range.getSheet();
if (e.value !== undefined && sheet.getName() == watchSheet) {
if (watchColumns.indexOf(range.getColumn()) > -1) {
var row = range.getRow();
if (row > headerRows) {
var autoCell = sheet.getRange(row, autoColumn);
if (!autoCell.getValue()) {
var data = sheet.getDataRange().getValues();
var temp = 1;
for (var i = headerRows, length = data.length; i < length; i++)
if (data[i][autoColumn - 1] > temp)
temp = data[i][autoColumn - 1];
autoCell.setValue(temp + 1);
}
}
}
}
}
Use documentLock to prevent any user of the current document from concurrently running a section of code.
I have searched many possible answers but cannot seem to find one that works. I have a Google Sheet with about 1600 rows that I need to split into about 70 different tabs (with about 20-30 rows in each one) based on the value in the column titled “room”. I have been sorting and then cutting and pasting but for 70+ tabs this is very tedious.
I can use the Query function but I still need to create a new tab, paste the function and update the parameter for that particular tab.
This script seemed pretty close:
ss = SpreadsheetApp.getActiveSpreadsheet();
itemName = 0;
itemDescription = 1;
image = 2;
purchasedBy = 3;
cost = 4;
room = 5;
isSharing = 6;
masterSheetName = "Master";
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Update Purchases')
.addItem('Add All Rows To Sheets', 'addAllRowsToSheets')
.addItem('Add Current Row To Sheet', 'addRowToNewSheet')
.addToUi();
}
function addRowToNewSheet() {
var s = ss.getActiveSheet();
var cell = s.getActiveCell();
var rowId = cell.getRow();
var range = s.getRange(rowId, 1, 1, s.getLastColumn());
var values = range.getValues()[0];
var roomName = values[room];
appendDataToSheet(s, rowId, values, roomName);
}
function addAllRowsToSheets(){
var s = ss.getActiveSheet();
var dataValues = s.getRange(2, 1, s.getLastRow()-1, s.getLastColumn()).getValues();
for(var i = 0; i < dataValues.length; i++){
var values = dataValues[i];
var rowId = 2 + i;
var roomName = values[room];
try{
appendDataToSheet(s, rowId, values, roomName);
}catch(err){};
}
}
function appendDataToSheet(s, rowId, data, roomName){
if(s.getName() != masterSheetName){
throw new Error("Can only add rows from 'Master' sheet - make sure sheet name is 'Master'");
}
var sheetNames = [sheet.getName() for each(sheet in ss.getSheets())];
var roomSheet;
if(sheetNames.indexOf(roomName) > -1){
roomSheet = ss.getSheetByName(roomName);
var rowIdValues = roomSheet.getRange(2, 1, roomSheet.getLastRow()-1, 1).getValues();
for(var i = 0; i < rowIdValues.length; i++){
if(rowIdValues[i] == rowId){
throw new Error( data[itemName] + " from row " + rowId + " already exists in sheet " + roomName + ".");
return;
}
}
}else{
roomSheet = ss.insertSheet(roomName);
var numCols = s.getLastColumn();
roomSheet.getRange(1, 1).setValue("Row Id");
s.getRange(1, 1, 1, numCols).copyValuesToRange(roomSheet, 2, numCols+1, 1, 1);
}
var rowIdArray = [rowId];
var updatedArray = rowIdArray.concat(data);
roomSheet.appendRow(updatedArray);
}
But I always get an unexpected token error on line 51 or 52:
var sheetNames = [sheet.getName() for each(sheet in ss.getSheets())];
(And obviously the column names, etc. are not necessarily correct for my data, I tried changing them to match what I needed. Not sure if that was part of the issue.)
Here is a sample of my data: https://docs.google.com/spreadsheets/d/1kpD88_wEA5YFh5DMMkubsTnFHeNxRQL-njd9Mv-C_lc/edit?usp=sharing
This should return two separate tabs/sheets based on room .
I am obviously not a programmer and do not know Visual Basic or Java or anything. I just know how to google and copy things....amazingly I often get it to work.
Let me know what else you need if you can help.
Try the below code:
'splitSheetIntoTabs' will split your master sheet in to separate sheets of 30 rows each. It will copy only the content not the background colors etc.
'deleteTabsOtherThanMaster' will revert the change done by 'splitSheetIntoTabs'. This function will help to revert the changes done by splitSheetIntoTabs.
function splitSheetIntoTabs() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
var header = rows[0];
var contents = rows.slice(1);
var totalRowsPerSheet = 30; // This value will change no of rows per sheet
//below we are chunking the toltal row we have into 30 rows each
var contentRowsPerSheet = contents.map( function(e,i){
return i%totalRowsPerSheet===0 ? contents.slice(i,i+totalRowsPerSheet) : null;
}).filter(function(e){ return e; });
contentRowsPerSheet.forEach(function(e){
//crate new sheet here
var currSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
//append the header
currSheet.appendRow(header);
//populate the rows
e.forEach(function(val){
currSheet.appendRow(val);
});
});
}
// use this function revert the sheets create by splitSheetIntoTabs()
function deleteTabsOtherThanMaster() {
var sheetNotToDelete ='Master';
var ss = SpreadsheetApp.getActive();
ss.getSheets().forEach(function(sheet){
if(sheet.getSheetName()!== sheetNotToDelete)
{
ss.deleteSheet(sheet);
}
});
}
I was using Kessy's nice script, but started having trouble when the data became very large, where the script timed out. I started looking for ways to reduce the amount of times the script read/wrote to the spreadsheet (rather than read/write one row at a time) and found this post https://stackoverflow.com/a/42633934
Using this principle and changing the loop in the script to have a loop within the loop helped reduce these calls. This means you can also avoid the second call to append rows (the "else"). My script is a little different to the examples, but basically ends something like:
`for (var i = 1; i < theEmails.length; i++) {
//Ignore blank Emails and sheets created
if (theEmails[i][0] !== "" && !completedSheets.includes(theEmails[i][0])) {
//Set the Sheet name = email address. Index the sheets so they appear last.
var currentSheet = theWorkbook.insertSheet(theEmails[i][0],4+i);
//append the header
//To avoid pasting formulas, we have to paste contents
headerFormat.copyTo(currentSheet.getRange(1,1),{contentsOnly:true});
//Now here find all the rows containing the same email address and append them
var theNewRows =[];
var b=0;
for(var j = 1; j < rows.length; j++)
{
if(rows[j][0] == theEmails[i][0]) {
theNewRows[b]=[];//Initial new array
theNewRows[b].push(rows[j][0],rows[j][1],rows[j][2],rows[j][3],rows[j][4],rows[j][5],rows[j][6],rows[j][7]);
b++;
}
}var outrng = currentSheet.getRange(2,1,theNewRows.length,8); //Make the output range the same size as the output array
outrng.setValues(theNewRows);
I found a table of ~1000 rows timed out, but with the new script took 6.5 secs. It might not be very neat, as I only dabble in script, but perhaps it helps.
I have done this script that successfully gets each room and creates a new sheet with the corresponding room name and adding all the rows with the same room.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
// This var will contain all the values from column C -> Room
var columnRoom = sheet.getRange("C:C").getValues();
// This var will contain all the rows
var rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
//Set the first row as the header
var header = rows[0];
//Store the rooms already created
var completedRooms = []
//The last created room
var last = columnRoom[1][0]
for (var i = 1; i < columnRoom.length; i++) {
//Check if the room is already done, if not go in and create the sheet
if(!completedRooms.includes(columnRoom[i][0])) {
//Set the Sheet name = room (except if there is no name, then = No Room)
if (columnRoom[i][0] === "") {
var currentSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet("No Room");
} else {
var currentSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(columnRoom[i][0]);
}
//append the header
currentSheet.appendRow(header);
currentSheet.appendRow(rows[i]);
completedRooms.push(columnRoom[i][0])
last = columnRoom[i][0]
} else if (last == columnRoom[i][0]) {
// If the room's sheet is created append the row to the sheet
var currentSheet = SpreadsheetApp.getActiveSpreadsheet()
currentSheet.appendRow(rows[i]);
}
}
}
Please test it and don't hesitate to comment for improvements.
I have a spreadsheet displaying information on two monitors. The file has anywhere from 5 to 30 tabs. I have been trying to get the following script with a time-driven trigger to move from one sheet tab to the next every minute, however I have two problems:
1) - I need the loop to skip four tabs (they have fixed names) and I currently can't figure out a workable solution, and
2) - the time driven trigger of 1 minute is not doing anything, not working.
Here are two scripts I've been testing and tweeking to see if I find the one that works:
Script 1:
function MoveNext() {
var spreadsheet = SpreadsheetApp.getActive();
var nextSheetIndex = spreadsheet.getActiveSheet().getIndex() + 1;
if (nextSheetIndex > spreadsheet.getSheets().length) { nextSheetIndex = 1; }
spreadsheet.setActiveSheet(spreadsheet.getSheets()[nextSheetIndex - 1],true);
And Script 2: this one aims at skipping the four tabs I don't want to loop:
var ss = SpreadsheetApp.getActive();
var sheets = ss.getSheets();
for (i = 0; i < sheets.length; i++) {
switch (sheets[i].getSheetName()) {
case "T1":
case "T0":
case "Summary Panel":
case "Flight Info":
case "Template":
break;
default:
var nextSheetIndex = ss.getActiveSheet().getIndex() + 1;
if (nextSheetIndex > ss.getSheets().length) {
nextSheetIndex = 1;
}
ss.setActiveSheet(ss.getSheets()[nextSheetIndex - 1], true);
You'll have to add your timer code yourself, but here is an example of how to move to the next "approved" sheet.
function MoveNext() {
var sheetsToSkip = ["Sheet3", "Sheet4", "Sheet7"];
var thisBook = SpreadsheetApp.getActive();
var sheetCount = thisBook.getNumSheets();
var thisSheet = SpreadsheetApp.getActiveSheet();
var thisSheetName = thisSheet.getName();
var thisSheetIndex = thisSheet.getIndex() - 1; //subtract one to get the array index
var allSheets = thisBook.getSheets();
var i = thisSheetIndex;
var notDone = true;
while (notDone) {
if (i == (sheetCount-1)) {
i = 0;
} else {
i++;
}
Logger.log('next sheet index is ' + i);
var nextName = allSheets[i].getName();
if (sheetsToSkip.indexOf(nextName) == -1) {
var nextSheet = allSheets[i];
Logger.log('next active sheet should be ' + nextSheet.getName());
thisBook.setActiveSheet(nextSheet);
notDone = false;
}
}
}