1

Thanks to the help of someone from Stack, the following script was provided to make a protection script I had written run faster. While the new script does apply protections except the specified ranges, users who are provided editor access are able to edit outside of the desired ranges.

My hope is to ensure that users are only able to enter data in specific ranges, but in order to enter data, they need editor access. Is it possible to restrict editors to only edit the desired ranges?

// 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);
}  

Edit: The solution provided by Tanaike works to restrict editors to me (the owner), but the script will be run by other users when they insert a row using the following:

function addNewApparelSKU() {
  const ss = SpreadsheetApp.getActive();
  const ui = SpreadsheetApp.getUi();
  const sheet = ss.getSheetByName('Apparel INV');
  const response = ui.prompt('Enter New SKU', ui.ButtonSet.OK_CANCEL);
  if (response.getSelectedButton() === ui.Button.OK) {
    const text = response.getResponseText();
    sheet.appendRow([text]);
    sheet.sort(1);
    myFunction(); //references the Protection script
  }
} 

When this script is used by another editor, it gives an error because the user can't insert a row due to the restrictions.

  • I thought that I might be able to understand your goal. So I proposed an answer. Could you please confirm it? If I misunderstood your question and that was not useful, I apologize. – Tanaike Dec 14 '21 at 00:57

1 Answers1

2

I believe your goal is as follows.

  • You want to protect the specific ranges in each sheet and want to make users edit only the specific ranges.
  • From your updated question, the script of addNewApparelSKU is run by clicking a button on Spreadsheet.

About the following script was provided to make a protection script I had written run faster., if the script of your previous question is used, how about the following modified script?

And, in this case, it is required to run the script (in this case, it's myFunction().) by the owner (you). For this, I would like to run this script using Web Apps. By this, the script can be run by the owner.

Usage:

1. Sample script:

Please copy and paste the following script to the script editor of Spreadsheet. And pleaes enable Sheets API at Advanced Google services.

And, please set your email address to const email = "###"; in myFunction.

function addNewApparelSKU() {
  // This is from addNewApparelSKU().
  const ss = SpreadsheetApp.getActive();
  const ui = SpreadsheetApp.getUi();
  const response = ui.prompt('Enter New SKU', ui.ButtonSet.OK_CANCEL);
  if (response.getSelectedButton() === ui.Button.OK) {
    const text = response.getResponseText();

    const webAppsUrl = "https://script.google.com/macros/s/###/exec"; // Pleas set your Web Apps URL.

    const url = webAppsUrl + "?text=" + text;
    const res = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
    // ui.alert(res.getContentText()); // You can see the response value using this line.
  }
}

function doGet(e) {
  const text = e.parameter.text;
  const sheet = SpreadsheetApp.getActive().getSheetByName('Apparel INV');
  sheet.appendRow([text]);
  sheet.sort(1);
  myFunction();
  return ContentService.createTextOutput(text);
}

// 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() {
  const email = "###"; // <--- Please set your email address.

  // 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: { editors: {users: [email]}, range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
  
  // 4. Request to Sheets API with the created request body.
  Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}

2. Deploy Web Apps.

The detailed information can be seen at the official document.

  1. On the script editor, at the top right of the script editor, please click "click Deploy" -> "New deployment".

  2. Please click "Select type" -> "Web App".

  3. Please input the information about the Web App in the fields under "Deployment configuration".

  4. Please select "Me" for "Execute as".

    • This is the importance of this workaround.
  5. Please select "Anyone" for "Who has access".

    • In your situation, I thought that this setting might be suitable.
  6. Please click "Deploy" button.

  7. Copy the URL of the Web App. It's like https://script.google.com/macros/s/###/exec.

  8. Please set the Web Apps URL to const url = "https://script.google.com/macros/s/###/exec"; in the above script.

  9. Please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this. You can see this flow at here.

3. Testing.

Please run addNewApparelSKU() by clicking the button. By this, the script is run by the owner.

Note:

  • When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this.
  • You can see the detail of this in the report of "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
  • My proposed script is a simple script. So please modify it for your actual situation.

References:

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • Hi Tanaike, I really appreciate your help through this. I apologize for the continued questions. Your solution succeeded in restricting access, but one thing I should have mentioned in the first place is, this script will run periodically even when I'm not the user. When I run this script under a separate account, it won't run because the cells are protected. – ThePinkBird Dec 14 '21 at 01:24
  • @Tyler Thank you for replying. In your actual situation, how is the script run? – Tanaike Dec 14 '21 at 01:25
  • Hi Tanaike, when I run the script as the owner, it works great. Other users will periodically have to insert rows, and the script I wrote will insert a row, then call the script you wrote here, so that the protections can be expanded. – ThePinkBird Dec 14 '21 at 01:35
  • @Tyler Thank you for replying. I have to apologize for my poor English skill. Unfortunately, from your replying, I cannot still understand how the script is run by the user. Can I ask you about the detail of `call the script`? – Tanaike Dec 14 '21 at 01:42
  • Hi Tanaike, no apologies needed! Your English is great! I edited the main post to clarify. The `addNewApparelSKU()` will call the `myFunction()` when it runs, and the `addNewApparelSKU()` will be used by the editors who are restricted. – ThePinkBird Dec 14 '21 at 01:51
  • @Tyler Thank you for replying. About `the script will be run by other users when they insert a cell`, how is the script `addNewApparelSKU()` run? It's a button? – Tanaike Dec 14 '21 at 02:06
  • Hi Tanaike, apologies, I mean when they insert a row, not a cell. The script will be run when the user clicks a drawing I inserted. – ThePinkBird Dec 14 '21 at 02:10
  • @Tyler Thank you for replying. In your situation, the script of `addNewApparelSKU()` is run by clicking a button on Spreadsheet. Is my understanding correct? – Tanaike Dec 14 '21 at 02:10
  • Hi Tanaike, yes, that is correct. – ThePinkBird Dec 14 '21 at 02:13
  • @Tyler Thank you for replying. From your replying, I updated my answer. Could you please confirm it? In this case, the flow for setting is important. So please be careful this. – Tanaike Dec 14 '21 at 02:37
  • Hi Tanaike, I wanted to clarify, what is the `Web Apps URL` that is supposed to be set? Is it the one at the top of my page when I enter the Apps Script? – ThePinkBird Dec 14 '21 at 03:01
  • @Tyler Thank you for replying. When the Web Apps is deployed, you can retrieve the URL of the Web Apps. This can be seen at the flow in my updated question. I apologize for this. Using Web Apps, the user can run the script as you (the owner of the script). I thought that by this, your goal can be achieved. – Tanaike Dec 14 '21 at 04:40
  • @Tyler The URL of Web Apps is like `https://script.google.com/macros/s/###/exec`. After you copy and paste this URL to my proposed script, please reflect the latest script to the Web Apps. This is an important point. You can see the flow of this at [here](https://gist.github.com/tanaikech/ebf92d8f427d02d53989d6c3464a9c43). – Tanaike Dec 14 '21 at 04:54
  • Hi Tanaike, I've never used Web Apps before, but I think I did it correctly. I selected type as Web App and deployed, copied the URL and pasted it in the correct location. When I run it as another user, I get the same error, that I'm trying to edit a protected cell. I think the problem is that it's executing the `addNewApparelSKU()` first, which is the script that inserts a row. If the protections can be removed, then a row inserted, then protections added again, I think that would do it. Unless I did something wrong. – ThePinkBird Dec 14 '21 at 05:11
  • @Tyler Thank you for replying. I noticed a modification point in my proposed script. I apologize for this. So I updated my answer. Could you please confirm it? Now, when I tested this script again, I could confirm that the script works. When you use this updated script, please reflect the latest script to the Web Apps and test it again. – Tanaike Dec 14 '21 at 05:28
  • 1
    Hi Tanaike, this seems to have done the trick! I really can't thank you enough! I appreciate your patience. – ThePinkBird Dec 14 '21 at 06:06
  • @Tyler Thank you for your response. – Tanaike Dec 14 '21 at 23:56
  • Thank you for your help. I really appreciate it. – ThePinkBird Dec 15 '21 at 03:14