2

I'm still learning the fundamentals of Javascript, but I know there's a way to simplify this one, I just can't figure it out. I've modified a script I found here: Protect spreadsheet then unprotect specific cells and ranges with script

I'm sharing a sheet with multiple users and want most of each sheet protected. The above linked script helped me ensure that the ranges my users need access to can be edited by all even as the sheets expand. However, the editable ranges are different on each sheet, so I've ended up recycling the unlockCertainRanges() portion to apply it to each sheet individually. This has made the script take up to 70 seconds to run. I'm pretty sure I can use an array and a for loop to run through each sheet, but I'm having trouble figuring it out.

This is what I have so far:

    function mainProtection(){ //Main function to run
  var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
  var disregard = ["NetSuite INV", "Sales", "Delivery Schedule", "TO", "APP Arch", "ACC Arch", "INV REF"]; //ADD SHEET NAMES HERE THAT YOU WANT TO BE DISREGARDED

  for(var x=0; x<sheets.length; x++){
    if(disregard.some(data => sheets[x].getName().includes(data))){ 
      //E.g. Disregard any sheet names added on the "disregard" array
    }else{
      unlockOrderingRanges(sheets[x]);
      unlockPendingTORanges(sheets[x]);
      unlockAccessoryRanges(sheets[x]);
      unlockApparelRanges(sheets[x]);
    }
  }
}


function unlockOrderingRanges(){ //Function to unlock ranges on Ordering spreadshseet
  var sheet = SpreadsheetApp.getActive().getSheetByName("Ordering");
  // Remove all range protections in the spreadsheet
  var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
  for (var i = 0; i < protections.length; i++) {
    var protection = protections[i];
    protection.remove();
  }

  var protection = sheet.protect();
  //restrict editors to owner
  protection.getRange().getA1Notation();
  var eds = protection.getEditors();
  protection.removeEditors(eds);

  //set unprotected ranges
  var ranges = protection.getUnprotectedRanges();
  var data = ["O5:P", "C2:E2"]; // ADD YOUR RANGES HERE
  data.forEach(res => { //LOOPS INTO EVERY ARRAY CONTAINING SPECIFIC RANGES
    ranges.push(sheet.getRange(res));
    protection.setUnprotectedRanges(ranges); //REMOVES THE PROTECTION ON THE RANGE
  });
}

I omitted the unlockPendingTORanges(); , unlockAccessoryRanges(); and unlockApparelRanges(); scripts for the sake of simplifying this post, as they are identical to the unlockOrderingRanges() script, they just change the defined sheet name and ranges.

Any guidance is greatly appreciated!

ETA details of the unlockPendingTORanges(); , unlockAccessoryRanges(); and unlockApparelRanges();

function unlockPendingTORanges(){ //Function to unlock ranges on Pending TOs spreadshseet
  var sheet = SpreadsheetApp.getActive().getSheetByName("Pending TOs");
  // Remove all range protections in the spreadsheet
  var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
  for (var i = 0; i < protections.length; i++) {
    var protection = protections[i];
    protection.remove();
  }

  var protection = sheet.protect();
  //restrict editors to owner
  protection.getRange().getA1Notation();
  var eds = protection.getEditors();
  protection.removeEditors(eds);

  //set unprotected ranges
  var ranges = protection.getUnprotectedRanges();
  var data = ["E6:H"]; // ADD YOUR RANGES HERE
  data.forEach(res => { //LOOPS INTO EVERY ARRAY CONTAINING SPECIFIC RANGES
    ranges.push(sheet.getRange(res));
    protection.setUnprotectedRanges(ranges); //REMOVES THE PROTECTION ON THE RANGE
  });
}


    function unlockAccessoryRanges(){ //Function to unlock ranges on Accessory INV spreadshseet
      var sheet = SpreadsheetApp.getActive().getSheetByName("Accessory INV");
      // Remove all range protections in the spreadsheet
      var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
      for (var i = 0; i < protections.length; i++) {
        var protection = protections[i];
        protection.remove();
      }
    
      var protection = sheet.protect();
      //restrict editors to owner
      protection.getRange().getA1Notation();
      var eds = protection.getEditors();
      protection.removeEditors(eds);
    
      //set unprotected ranges
      var ranges = protection.getUnprotectedRanges();
      var data = ["E5:H"]; // ADD YOUR RANGES HERE
      data.forEach(res => { //LOOPS INTO EVERY ARRAY CONTAINING SPECIFIC RANGES
        ranges.push(sheet.getRange(res));
        protection.setUnprotectedRanges(ranges); //REMOVES THE PROTECTION ON THE RANGE
      });
    }
    
    
    function unlockApparelRanges(){ //Function to unlock ranges on Apparel INV spreadshseet
      var sheet = SpreadsheetApp.getActive().getSheetByName("Apparel INV");
      // Remove all range protections in the spreadsheet
      var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
      for (var i = 0; i < protections.length; i++) {
        var protection = protections[i];
        protection.remove();
      }
    
      var protection = sheet.protect();
      //restrict editors to owner
      protection.getRange().getA1Notation();
      var eds = protection.getEditors();
      protection.removeEditors(eds);
    
      //set unprotected ranges
      var ranges = protection.getUnprotectedRanges();
      var data = ["E5:F"]; // ADD YOUR RANGES HERE
      data.forEach(res => { //LOOPS INTO EVERY ARRAY CONTAINING SPECIFIC RANGES
        ranges.push(sheet.getRange(res));
        protection.setUnprotectedRanges(ranges); //REMOVES THE PROTECTION ON THE RANGE
      });
    }
Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • Can I ask you about the detail of `they just change the defined sheet name and ranges.`? – Tanaike Dec 10 '21 at 05:37
  • @Tanaike, I edited the original post to clarify those. Apologies if it was unclear, I was trying to keep the post shorter. Thank you! – ThePinkBird Dec 10 '21 at 17:22

1 Answers1

1

I believe your goal is as follows.

  • You want to reduce the process cost of your script.

In this case, I would like to propose using Sheets API. I thought that when Sheets API is used for your script, the process cost will be reduced a little. When Sheets API is reflected in your script, it becomes as follows.

Modified script:

Before you use this script, please enable Sheets API at Advanced Google Services.

// This script is from https://tanaikech.github.io/2017/07/31/converting-a1notation-to-gridrange-for-google-sheets-api/
function a1notation2gridrange1(a1notation) {
  var data = a1notation.match(/(^.+)!(.+):(.+$)/);
  var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
  var range = ss.getRange(data[2] + ":" + data[3]);
  var gridRange = {
    sheetId: ss.getSheetId(),
    startRowIndex: range.getRow() - 1,
    endRowIndex: range.getRow() - 1 + range.getNumRows(),
    startColumnIndex: range.getColumn() - 1,
    endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
  };
  if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
  if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
  return gridRange;
}

// Please run this function.
function myFunction() {
  // Please set your sheet names and unprotected ranges you want to use.
  const obj = [ 
  { sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },  
  { sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },  
  { sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },  
  {sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] }, 
  {sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
];

  // 1. Retrieve sheet IDs and protected range IDs.
  const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
  const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges: obj.map(({ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
  const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
    if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push(protectedRanges.map(({ protectedRangeId }) => protectedRangeId));
    o.sheetIds.push(sheetId);
    return o;
  }, { protectedRangeIds: [], sheetIds: [] });
  
  // 2. Convert A1Notation to Gridrange.
  const gridranges = obj.map(({ sheetName, unprotectedRanges }, i) => unprotectedRanges.map(f => a1notation2gridrange1(`${sheetName}!${f}`)));

  // 3. Create request body.
  const deleteProptectedRanges = protectedRangeIds.flatMap(e => e.map(id => ({ deleteProtectedRange: { protectedRangeId: id } })));
  const protects = sheetIds.map((sheetId, i) => ({ addProtectedRange: { protectedRange: { range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
  
  // 4. Request to Sheets API with the created request body.
  Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}

Note:

  • From I omitted the unlockPendingTORanges(); , unlockAccessoryRanges(); and unlockApparelRanges(); scripts for the sake of simplifying this post, as they are identical to the unlockOrderingRanges() script, they just change the defined sheet name and ranges. of your question, unfortunately, I couldn't understand other sheet names and unprotected ranges. So in this modified script, one pattern is used. Please add your other patterns to the variable of obj.

References:

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • 'If I understood: `const obj = [ { sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] }, { sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] }, { sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] }, {sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] }, ];` – ThePinkBird Dec 10 '21 at 17:38
  • @Tyler Thank you for replying. When you use your modified `obj`, what result will you obtain? If my proposed script was not useful, I have to apologize for it. – Tanaike Dec 10 '21 at 23:51
  • I'm pretty sure I enabled the Sheets API correctly, but I've never done it before, so maybe that's it? I get the following: TypeError: Cannot read property 'map' of undefined sheets.reduce.protectedRangeIds @ APIProtectionsTest.gs:34 myFunction @ APIProtectionsTest.gs:33 – ThePinkBird Dec 11 '21 at 22:42
  • @Tyler Thank you for replying. I have to apologize for my poor skill. Unfortunately, I cannot replicate your situation. When I tested my proposed script, no error occurs. I deeply apologize that no error occurs. So can you provide the sample Spreadsheet including your current script for correctly replicating your issue? By this, I would like to confirm it. – Tanaike Dec 11 '21 at 23:42
  • I appreciate your help! Due to confidentiality agreements, I had to omit some data, but otherwise this sheet is identical to the one I use: https://docs.google.com/spreadsheets/d/1DxafMMZL7Nc2M4A41Dzssf9dgrzYZgsX1qKgtfhLMlM/edit?usp=sharing – ThePinkBird Dec 12 '21 at 02:15
  • @Tyler Thank you for replying and providing your sample Spreadsheet. From your sample Spreadsheet, I adjusted my proposed script. So I updated my script. Could you please confirm it? In your script, `@OnlyCurrentDoc` is used. So, also, I modified `a1notation2gridrange1`. So please copy and paste both `a1notation2gridrange1` and `myFunction` and test it again. – Tanaike Dec 12 '21 at 02:29
  • Hi Tanaike, I still get the same TypeError: Cannot read property 'map'... I also noticed that, while it lets me import the `a1notation2gridrange1` script, it doesn't let me import the `myFunction` script. The only way I can run it is from the Apps Script itself. Not sure if that's related but I've never seen that before. I really appreciate your help through this though! – ThePinkBird Dec 12 '21 at 03:00
  • Hi Tanaike, never mind, I misunderstood! I didn't realize it was the script here in the answer that you adjusted. I just copied it over and it looks like it worked! I'm going to test it on my primary sheet now. – ThePinkBird Dec 12 '21 at 03:06
  • IT WORKED! Thank you so much for your help, Tanaike! I appreciate your patience and guidance. And for the new topic I need to explore: API. Seriously, I can't thank you enough. – ThePinkBird Dec 12 '21 at 03:14
  • 1
    Hi Tanaike, thank you again for your help! You single handedly made my sheet more user friendly and capable of being implemented in other locations without my supervision. It works wonders. I really appreciate it. – ThePinkBird Dec 12 '21 at 15:58
  • Hi Tanaike, I've run into an issue with the script that I'm hoping you can help me with. While the script works to apply the protections, it doesn't restrict editing ability to only myself. I gave editor access to my coworker, and he was able to edit outside the unprotected ranges. Is this something you can help me with? Thank you! – ThePinkBird Dec 14 '21 at 00:14
  • @Tyler I would like to support you. But I have to apologize for my poor English skill. Unfortunately, I cannot understand about the new issue in your replying. So can you post it as a new question by including more information? By this, I would like to try to understand it, and also that will help a lot of users think of your question. If you can cooperate to resolve your new issue, I'm glad. Can you cooperate to do it? – Tanaike Dec 14 '21 at 00:18
  • Hi Tanaike, I took your advice and posted another question here: https://stackoverflow.com/questions/70342492/restrict-editors-to-specific-ranges-script – ThePinkBird Dec 14 '21 at 00:36
  • @Tyler Thank you for your response. I would like to check it. – Tanaike Dec 14 '21 at 00:39