Restrict Editors to Specific Ranges Script - javascript

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 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.
On the script editor, at the top right of the script editor, please click "click Deploy" -> "New deployment".
Please click "Select type" -> "Web App".
Please input the information about the Web App in the fields under "Deployment configuration".
Please select "Me" for "Execute as".
This is the importance of this workaround.
Please select "Anyone" for "Who has access".
In your situation, I thought that this setting might be suitable.
Please click "Deploy" button.
Copy the URL of the Web App. It's like https://script.google.com/macros/s/###/exec.
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".
Please set the Web Apps URL to const url = "https://script.google.com/macros/s/###/exec"; in the above 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 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:
Web Apps
Taking advantage of Web Apps with Google Apps Script

Related

Triggering function from one master spreadsheet in other spreadsheet documents

I have a bunch of spreadsheets containing financial forecast. At the end of each month I make a copy to lock in the months forecast in every sheet. This works well. The problem is that I have to open all the sheets separately in order to activate the script in each sheet. So I was wondering if there is a way to trigger the script via a button from a master spreadsheet that looks something like this: https://docs.google.com/spreadsheets/d/1YA6twVP3_oSJttcLx5JRdm8TO01zKFr2k_D9lrlc1eE/edit#gid=0
The script I'd like to trigger looks like this:
function CopyMonth() {
const sh = SpreadsheetApp.getActive();
let ss = sh.duplicateActiveSheet();
let name = ss.getRange("a26").getDisplayValue();
ss.setName(name);
var sheet = SpreadsheetApp.getActiveSheet();
var rangeToCopy = sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns());
rangeToCopy.copyTo(sheet.getRange(1, 1), {contentsOnly: true});
}
The time and date I lock the forecast varies so it cannot be activated by a specific time or something like that.
The marked duplicate does not address my issue. The subject referenced to explains how to create a button to run a script. I'm asking how to run a script I've made in one document from an entirely different document. My problem is that I have 8 document I have to open and press a button today to run my script. I want to be able to run the scrip in all 8 documents from a master document instead.
Provided that all the spreadsheets you want to process are in the same folder, with no other spreadsheets in that folder or its subfolders, you can use something like this:
/**
* Makes a values only copy of the first tab in all
* spreadsheets in a folder and its subfolders.
*/
function insertValuesOnlyMonthTabInManySpreadsheets() {
// version 1.0, written by --Hyde, 14 December 2022
// - see https://stackoverflow.com/q/74801764/13045193
const folderName = '...put folder name here...';
const _action = (file) => {
const ss = getSpreadsheetFromFile_(file);
if (!ss) {
return;
}
const firstTab = ss.getSheets()[0];
const tabName = firstTab.getRange('A26').getDisplayValue();
let newTab;
try {
newTab = firstTab.copyTo(ss);
newTab.setName(tabName);
} catch (error) {
handleError_(error);
if (!newTab) {
return;
}
}
const rangeToCopyAsValuesOnly = firstTab.getDataRange();
rangeToCopyAsValuesOnly.copyTo(newTab.getRange(1, 1), { contentsOnly: true });
console.log(`Processed '${ss.getName()}'.`);
};
const folder = DriveApp.getFoldersByName(folderName).next();
processFilesInFolderRecursively_(folder, _action, handleError_);
}
For that to work, you will need to paste the getSpreadsheetFromFile_(), processFilesInFolderRecursively_() and handleError_() functions in the script project. You can get them from the processFilesInFolderRecursively_ script.

API request not updating variable in the first request. Second request is able to output the correct information

I am building an application to collate all the indicators that I use while investing in the stock market, as a personal project. I am currently making use of the yahoo-finance API. Given below is my code for one of the data loading functions, that will later be used with Tulind package.
var yahooFinance = require("yahoo-finance");
var returnValue = [];
const loadData = (symbol, from, to, freq) => {
let open = [];
let close = [];
let high = [];
let low = [];
let volume = [];
let updateValues = () => {
returnValue.map((e) => {
open.push(e.open);
close.push(e.adjClose);
high.push(e.high);
low.push(e.low);
volume.push(e.volume);
});
};
const data = yahooFinance.historical(
{
symbol: symbol,
from: from,
to: to,
freq: freq,
},
function (error, quotes) {
if (error)
console.log("Error in server/indicatorCalc/parent_calc.js", err);
else {
returnValue = quotes;
}
console.log("Data loaded in parent_calc.js");
}
);
updateValues();
return {
open,
close,
high,
low,
volume,
};
};
// console.log(tulind.indicators);
module.exports = { loadData };
Given above is the function, which has an API endpoint at localhost:5000/indicatorParent/
For testing purposes, the API is being called using the thunderclient extension on VSCode.
The first request sent with the required parameters is outputting an empty value (or the previous value if this is not the first request). When I click the send button for the second time, it gets updated with the correct/expected values. I want to know how to rectify this apparent "lag" in the updating of the returnValue variable. I am open to suggestions in changes in the flow of code as well.

How to run a function based on an answer

I am trying to figure out how to make a script to run a function based on an answer provided, at the moment my script creates and populates a word doc with answers provided from a google form. I want to streamline it so that I can use one google form to create different documents based on an answer provided rather than creating multiple google forms. I am very new to JavaScript and I am pretty sure I need an if statement at the beginning of everything, but I don't know what I should write or where it should go. This is my script:
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('AutoFill Docs');
menu.addItem('Create New Docs', 'createNewGoogleDocs');
menu.addToUi();
}
function createNewGoogleDocs(){
const googleDocTemplate = DriveApp.getFileById('google file goes here');
const destinationFolder = DriveApp.getFolderById('google folder goes here');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Response');
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row,index) {
if (index === 0) return;
if (row[9]) return;
const copy = googleDocTemplate.makeCopy(`${row[5]} document name goes here`, destinationFolder);
const doc = DocumentApp.openById(copy.getId())
const body = doc.getBody();
body.replaceText('{{Name}}', row[5]);
body.replaceText('{{To}}', row[6]);
body.replaceText('{{Items}}', row[7]);
body.replaceText('{{Reasoning}}', row[8])
body.replaceText('{{Submitter}}', row[1]);
body.replaceText('{{Role}}', row[2]);
body.replaceText('{{Time}}', row[3]);
body.replaceText('{{Discord}}', row[4]);
doc.saveAndClose();
const url = doc.getUrl();
sheet.getRange(index + 1, 10).setValue(url)
})
}

Trying to import all google sheets files within a folder to a spreadsheet, all column headers are the same

I need to be able to take all files from a a folder within drive and input the data into the spreadsheet. I am also creating a Menu Tab so I can just Run the script without going to editor. It would be great if I can create a way enter names of existing folder name without always going to the script in order to take out that extra step. This is the script I am using. I really need assistance with this.
function importTimesheets() {
var spreadsheets = DriveApp.
getFolderById("").
getFilesByType(MimeType.GOOGLE_SHEETS);
var data = [];
while (spreadsheets.hasNext()) {
var currentSpreadsheet = SpreadsheetApp.openById(spreadsheets.next().getId());
data = data.concat(currentSpreadsheet
.getSheetByName('Timesheet')
.getRange("A3:L10")
.getValues()
);
}
SpreadsheetApp.
getActiveSheet().
getRange(1, 1, data.length, data[0].length).
setValues(data);
}
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Generate Timesheets')
.addItem('Generate', 'importTimesheets')
As far as I understand you are trying to find a solution for this line of code, whereby you currently have to enter the Id manually.
DriveApp.getFolderById("")
My suggestions would be to prompt the user for the folder Id, because prompting for the folder name may cause errors if more than one folder has the same name. My suggestion is implemented as follows:
const folderId = SpreadsheetApp.getUi().prompt("Please enter the Folder Id").getResponseText()
const folder = DriveApp.getFolderById(folderId)
You could also use the Google File/Folder picker, as described here to search and select the folder.
Try this:
var level=0;
function getFnF(folder = DriveApp.getRootFolder()) {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1')
const files=folder.getFilesByType(MimeType.GOOGLE_SHEETS);
while(files.hasNext()) {
let file=files.next();
let firg=sh.getRange(sh.getLastRow() + 1,level + 1);
firg.setValue(Utilities.formatString('File: %s', file.getName()));//need editing
}
const subfolders=folder.getFolders()
while(subfolders.hasNext()) {
let subfolder=subfolders.next();
let forg=sh.getRange(sh.getLastRow() + 1,level + 1);
forg.setValue(Utilities.formatString('Fldr: %s', subfolder.getName()));//needs editing
level++;
getFnF(subfolder);
}
level--;
}
function runThisFirst() {
let r = SpreadsheetApp.getUi().prompt('Folder id','Enter Folder Id',SpreadsheetApp.getUi().ButtonSet.OK);
let folder = DriveApp.getFolderById(r.getResponseText())
getFnF(folder);
}

Insert Line breaks before text in Google Apps Script

I need to insert some line breaks before certain text in a Google Document.
Tried this approach but get errors:
var body = DocumentApp.getActiveDocument().getBody();
var pattern = "WORD 1";
var found = body.findText(pattern);
var parent = found.getElement().getParent();
var index = body.getChildIndex(parent);
// or parent.getChildIndex(parent);
body.insertParagraph(index, "");
Any idea on how to do this?
Appreciate the help!
For example, as a simple modification, how about modifying the script of https://stackoverflow.com/a/65745933 in your previous question?
In this case, InsertTextRequest is used instead of InsertPageBreakRequest.
Modified script:
Please copy and paste the following script to the script editor of Google Document, and please set searchPattern. And, please enable Google Docs API at Advanced Google services.
function myFunction() {
const searchText = "WORD 1"; // Please set text. This script inserts the pagebreak before this text.
// 1. Retrieve all contents from Google Document using the method of "documents.get" in Docs API.
const docId = DocumentApp.getActiveDocument().getId();
const res = Docs.Documents.get(docId);
// 2. Create the request body for using the method of "documents.batchUpdate" in Docs API.
let offset = 0;
const requests = res.body.content.reduce((ar, e) => {
if (e.paragraph) {
e.paragraph.elements.forEach(f => {
if (f.textRun) {
const re = new RegExp(searchText, "g");
let p = null;
while (p = re.exec(f.textRun.content)) {
ar.push({insertText: {location: {index: p.index + offset},text: "\n"}});
}
}
})
}
offset = e.endIndex;
return ar;
}, []).reverse();
// 3. Request the request body to the method of "documents.batchUpdate" in Docs API.
Docs.Documents.batchUpdate({requests: requests}, docId);
}
Result:
When above script is used, the following result is obtained.
From:
To:
Note:
When you don't want to directly use Advanced Google services like your previous question, please modify the 2nd script of https://stackoverflow.com/a/65745933 is as follows.
From
ar.push({insertPageBreak: {location: {index: p.index + offset}}});
To
ar.push({insertText: {location: {index: p.index + offset},text: "\n"}});
References:
Method: documents.get
Method: documents.batchUpdate
InsertTextRequest

Categories

Resources