Shared spreadsheet by link & unprotected range - google-apps-script

Here is my problem. I am generating some sheets in a shared Spreadsheet document. It's shared by link. The idea is to lockdown everyone except the Owner of the spreadsheet. All ranges to be locked down has been locked by the following code :
range.protect().setDescription("Locked");
All other ranges (that can be modified by anyone) are left without calling protect() on them.
So I used the examples at Protection class and I come with this code that I ran in debug mode as the owner :
let rangeProtect = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (let i = 0; i < rangeProtect.length; i++) {
let protection = rangeProtect[i];
if (protection.canEdit() && (protection.getDescription() == "Locked")) {
let editors = ss.getEditors();
for (let j = 0; j < editors.length; j++) {
ss.removeEditor(editors[j]);
}
// Add only owner
ss.addEditor(ownerEmail);
}
}
Unfortunately this is not working. Every person who use the link can still edit everything. But if I modify by hand each protected range with the name "locked" to 'owner only' it works...
So can you tell me what I am doing wrong ?
I tried also this approach:
let protection = sheet.protect().setDescription('Locked');
let unprotected = sheet.getRange("B1:C10");
protection.setUnprotectedRanges([unprotected]);
The problem stay the same :
If I setup the shared link to "View" i cannot modify anything despite the setUnprotectedRanges() call. Everything is locked.
If I setup the shared link to "Edit", I can see that the sheet is locked correctly with exception of that range (as seen when opening the "ranges and sheets protected" from Data menu) but still all person using the link can modify anything in the sheet.
I am stuggling with this problem for days now and I'm running out of idea.
I really tried a LOT of things. So if you know how to do this properly could you give a piece of working code/shared document sample to accomplish this ? Let's say :
Create two ranges with "Locked" description (ex: First Row & First Column)
Set the correct protection on "Locked" ranges (Edition by Owner only)
Anybody with the link could modify all but locked ranges
Using a shared document by link
Any help would be greatly appreciated ! Really !
Note: This code is written in Typescript because I'm using CLASP that convert everything into Google Apps Script's code.

Related

Google Sheets App Script Edit Protected Cell or Object Error

I have a Google Sheets script that I've been using for the past 2+ years that grabs a chunk of data and transcribes it into a pair of tally sheets, then clears the cells that had been filled creating said chunk of data so the process can be started over. This is used for an inventory calculation system. The script has reliably worked up until roughly 3 weeks ago, but I now encounter an edit protection error when users with limited access to the sheet attempt to run the script. None of the editable cells/ranges reference in the script are locked to any user, but the sheet does have protection on cells I do not want anybody to make inadvertent changes to.
The script is:
function CopyErADD() {
var sss=SpreadsheetApp.getActiveSpreadsheet();
var sheet = sss.getSheetByName('ADDER'); //Entry Sheet
var rangeIn = sheet.getRange('B32:N45'); //Range to copy into "Incoming" sheet
var dataIn = rangeIn.getValues();
var ts = sss.getSheetByName('Incoming'); //Tally sheet in
ts.getRange(ts.getLastRow()+1, 1, dataIn.length, dataIn[0].length).setValues(dataIn);
var rangeOut = sheet.getRange('B48:O54'); //Range to copy into "Outgoing" sheet
var dataOut = rangeOut.getValues();
var tss = sss.getSheetByName('Outgoing'); //Tally sheet out
tss.getRange(tss.getLastRow()+1, 1, dataOut.length, dataOut[0].length).setValues(dataOut);
SpreadsheetApp.flush() // NEWLY ADDED PER METAMAN'S SUGGESTION
sheet.getRange('E2:E5').clearContent();
sheet.getRange('B7:B20').clearContent();
sheet.getRange('E7:H20').clearContent();
sheet.getRange('I7:I20').setValue('kg');
sheet.getRange('L7:L20').clearContent();
sheet.getRange('B24:B29').clearContent();
sheet.getRange('J24:J29').clearContent();
}
I also have an "erase only" script that runs the second second part of the script only (if data has been entered incorrectly) that executes perfectly fine. It is only when coupled with the copy/transcribe portion of the script that the protection error occurs.
function ErADD() {
var sss=SpreadsheetApp.getActiveSpreadsheet();
var sheet = sss.getSheetByName('ADDER');
sheet.getRange('E2:E5').clearContent();
sheet.getRange('B7:B20').clearContent();
sheet.getRange('E7:H20').clearContent();
sheet.getRange('I7:I20').setValue('kg');
sheet.getRange('L7:L20').clearContent();
sheet.getRange('B24:B29').clearContent();
sheet.getRange('J24:J29').clearContent();
}
Commenting out the sheet.getRange.... etc clearContent and setValue portion of the first script allows it to complete successfully, so I attempted to create a master function that calls the CopyErAdd script (sans clear portion) and then calls the ErADD script, and I encounter the same error. Both script can be run on their own successfully, but when combined the erase portion encounters the error.
Does anybody see any issues that I am missing, or did something occur over the past few weeks that I'm not aware of that could cause this protection error?
I appreciate any ideas anybody might have.
Edit - Thank you MetaMan for the tips for making the script more efficient.
As for the protection, "Incoming" and "Outgoing" copy destination sheets are completely open, no protection at all. "ADDER" sheet is protected except certain cells that are edited by user, and are referenced in the script.
E2:E5 (actually E2:I5 open, since the macro button sits on top of the F2:I5 range), B7:B20, E7:I20, L7:L20, B24:B29, J24:J29, All unprotected.
Adding the line
SpreadsheetApp.flush()
in between the copy section and clearing section of the code worked (as shown in the updated code snippet). I'm no longer encountering a range protection error.
I really appreciate the fix, but I'm boggled over why I've never needed that line until recently. I guess you don't need flush() until you do.
You could replace this:
sheet.getRange('I7').setValue('kg');
sheet.getRange('I8').setValue('kg');
sheet.getRange('I9').setValue('kg');
sheet.getRange('I10').setValue('kg');
sheet.getRange('I11').setValue('kg');
sheet.getRange('I12').setValue('kg');
sheet.getRange('I13').setValue('kg');
sheet.getRange('I14').setValue('kg');
sheet.getRange('I15').setValue('kg');
sheet.getRange('I16').setValue('kg');
sheet.getRange('I17').setValue('kg');
sheet.getRange('I18').setValue('kg');
sheet.getRange('I19').setValue('kg');
sheet.getRange('I20').setValue('kg');
with this:
sheet.getRange('I7:I20).setValue('kg');
also replace this:
var ss = sss.getSheetByName('ADDER'); //Entry sheet
var sheet = SpreadsheetApp.getActive().getSheetByName('ADDER');
with this:
var sheet = sss.getSheetByName('ADDER);
If you are doing anything immediate after this function with the data then you might wish to add SpreadsheetApp.flush()
I can't comment on the protection since none of that information was provided.

How to copy Table from a Spreadsheet to a Google Doc as `link to Spreadsheet`?

When users copy a range manually from a spreadsheet and paste it into a Google Doc, it prompts the option to "link to spreadsheet" and "paste normally".
I needed to do the functionality of "linked to a spreadsheet using google apps script"
Use case context
We have a spreadsheet with a table we are trying to copy to our final document where the user can add additional stuff.
So if for any reason some value needs to change in the spreadsheet, our users just want to simply refresh the table data.
Answer
This is not possible without a workaround.
In the table object in the documentation there is no method for inserting a table with a "link to spreadsheet" as there is in the UI. Seeking in the table object in the Docs API there is nothing exposed there either. You could file a feature request for this in the Issue Tracker, at the moment, I can see no feature requests for this.
A potential avenue for a workaround
Any workaround will not be as seamless as the UI, though if you want to automate the inserting of a table from a specific source, here is an example of that.
Please note that this is only a starting point. You should experiment with this and then if you need more features and run into problems, you should ask new questions about the specific problems your are having.
Introduction and initial steps
This script will take a table from a Sheet. This table should be the only thing in the sheet. The way the script is set up, is that it uses getDataRange which automatically selects all the data in a Sheet. You can modify this for your use case depending on how your spreadsheet is set up.
Then it will append the table to the end of the document. You can modify this depending on your needs. It will keep most of the formatting.
The styles will not match exactly, though again, this is something you can iron out the details depending on your use case.
Instructions
Get the id number of the spreadsheet and document
Create a script file
Copy this function:
function appendTable() {
// Replace these values with your Sheet ID, Document ID, and Sheet Name
let ssId = '<your spreadsheet id>' // REPLACE
let docId = '<your document id>' // REPLACE
let sheetName = '<your sheet name>' // REPLACE
// Sheet
let range = SpreadsheetApp.openById(ssId).getSheetByName(sheetName).getDataRange()
let values = range.getValues();
let backgroundColors = range.getBackgrounds();
let styles = range.getTextStyles();
// Document
let body = DocumentApp.openById(docId).getBody();
let table = body.appendTable(values);
for (let i=0; i<table.getNumRows(); i++) {
for (let j=0; j<table.getRow(i).getNumCells(); j++) {
let docStyles = {};
docStyles[DocumentApp.Attribute.BACKGROUND_COLOR] = backgroundColors[i][j];
docStyles[DocumentApp.Attribute.FONT_SIZE] = styles[i][j].getFontSize();
docStyles[DocumentApp.Attribute.BOLD] = styles[i][j].isBold()
table.getRow(i).getCell(j).setAttributes(docStyles);
}
}
}
Run the script!
Design how you want the user to run this and maybe set this up an add-on. Though this is out of the scope of this question.
References
Main Apps Script Page
Sheets Reference
SpreadsheetApp
getDataRange
getTextStyles - get the format from the sheet.
DocumentApp
Apps Script Docs Table
Doc Enum Attribute - for styling the table in Docs.

Automatically copy information once text in cell was changed

I need some help from the experts here.
Please take a look at this spreadsheet:
https://docs.google.com/spreadsheets/d/1tSl8LxhLGoQMVT_83Ev4jMu_Fo1AW8lN6N8Yw8kX44U/edit#gid=640017957
Here is the story (in short):
Our company deals with many different sellers of e-commerce businesses that want to sell their business to new owners. For each seller, we have a specific sheet (seller A, seller B, seller C in the example spreadsheet above) where we fill out various data, including the last action and next action that we took/need to take when handling the communication regarding the sell of the business with each of our potential buyers. We try to find an easy way to automatically store the data that we fill out in the “last step” column so that every time we update a cell in this column, the data will be automatically copied to a sheet that store all the communication history with each buyer for each deal.
I thought about creating a tab such as the “all actions” sheet in the example spreadsheet above, where every time we update the data in the “last action” column, a new row will be automatically added to the “all actions” sheet with the relevant data shown there.
Is there any way to achieve this goal? If not, will you recommend a different method to get similar results so that we can automatically store (and see once needed) all the data that was entered in the past in the “last action” column?
You can use triggers to do this.
In your sheet, click Tools > Script Editor.
In the script editor, click Edit -> Current Project Triggers
In that window, in the lower right, there is a button to "Add Trigger".
When you add the trigger set the "Select Event Type" to "On Change".
Reference the function you wish to run that will do the work of adding the information.
You will need to write the JavaScript function (in the script editor) to do the insert.
If you are not familiar with JavaScript and using it to work with Google Sheets, the learning curve isn't very steep to do this basic thing. I recommend digging in. The power you will wield with your spreadsheets is well worth the time.
You need to use Google Apps Script and the onEdit trigger in particular.
Try this:
function onEdit(e) {
var row = e.range.getRow();
var col = e.range.getColumn();
if ( e.source.getActiveSheet().getName() != "All actions" && row>1 && col==2 ){
s_name = e.source.getActiveSheet().getName();
b_name = e.source.getActiveSheet().getRange(row,1).getValue();
a_taken = e.source.getActiveSheet().getRange(row,2).getValue();
e.source.getSheetByName("All actions").appendRow([s_name,b_name,a_taken,new Date()])
}
}
In order to use this function you need to go to the Spreadsheet file menu; click on Tools => Script editor, clear the code.gs file and copy the aforementioned code snippet. Then, everytime a seller edits the last action column the relevant information will be appended to the All actions sheet.
Try this:
function onEdit(e) {
var sh=e.range.getSheet();
var s_name=sh.getName();
if ( s_name!= "All actions" && e.range.rowStart>1 && e.range.columnStart==2 ){
values=sh.getRange(e.range.rowStart,1,1,2).getValues()[0];
e.source.getSheetByName("All actions").appendRow([s_name,values[0],values[1],new Date()])
}
}

Script in Google Apps Script that copies values in a range to another sheet

I am working on a tool that will allow my company to track its financial investments.
I need to create a simple data entry form for users to input transaction data, which will then populate a master sheet which is the basis for the analyses the tool does. I cannot do this via Google Forms because the data entry form uses a lot of conditional formatting based on other data in the sheet.
I have uploaded a very simplified sheet to illustrate what I need: this
What I am looking for is a script that, upon clicking Submit in the Data Entry sheet, copies the values (NOT the formulas) in B12:E12 to the first empty row (in this case, row 8) in the "Master Table" sheet. Ideally, clicking "Submit" will also clear the data entry fields in C4:C7 in the "Data Entry" sheet.
I have looked through various forums for a solution but have not found anything that does exactly that. I am sorry to say I am a complete newbie at Google Apps Script, therefore I could not write my own code to share, which I am aware is customary when asking a question here.
If anyone could point me in the right direction regardless, it would be much appreciated. I am currently trying to learn JavaScript and Google Apps Script using online resources, but for this specific project, it would take too long for me to reach a level where I could help myself.
Thank you very much, GEOWill!
Your answer solved my problem and thanks to your comments, I was able to understand exactly what your code does. I changed the code only to remove the Menu (but thank you very much for showing me how that is done anyway) - I tied the function to a Submit button inserted as a drawing. I also added some code to clear the contents of the entry range after clicking the button (this was suggested by someone else).
The final code that I used is:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var entry_sheet = ss.getSheetByName("Data Entry");
var master_sheet = ss.getSheetByName("Master Table");
function mySubmit() {
var entry_range = entry_sheet.getRange("B12:E12")
var val = entry_sheet.getRange("B12:E12").getDisplayValues().reduce(function (a, b) {
return a.concat(b);
});
Logger.log(val);
master_sheet.appendRow(val);
entry_sheet.getRange("C4:C7").clearContent()
}
I hope this helps others with a similar problem! Love how supportive this community is. Thanks for helping out.
I would begin by using a menu function to run your code in (rather than a cell button). You could use a cell button, but I believe you need to insert an image and assign a javascript function to that image anyways.
Basically, you begin by going to tools, script editor. Create an onOpen function and create a trigger that runs the onOpen() function each time the spreadsheet is opened. Inside the onOpen() function, we create a menu into which a physical menu item (a kind of button) exists (called 'Submit'). Finally, we associate a function to this button (I called it mySubmit()).
Inside of the mySubmit() function is where most of the functionality you are looking for exists. At this point, it is just a matter of copying from one range of cells and pasting them to another range of cells. For this, you'll notice that I had to setup a few variables ahead of time (ss, entry_sheet, master_sheet, entry_range, and master_row).
One last thing, you may want to protect the Master table sheet because if someone accidentally edits a cell beyond the last one edited, the input row would be copied to that row (due to how the getLastRow() function operates).
Hope this helps!
var ss = SpreadsheetApp.getActiveSpreadsheet();
var entry_sheet = ss.getSheetByName("Data Entry");
var master_sheet = ss.getSheetByName("Master Table");
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('MyMenu')
.addItem('Submit', 'mySubmit')
.addToUi();
}
function mySubmit() {
var entry_range = entry_sheet.getRange("B12:E12");
var master_row = (master_sheet.getLastRow() + 1);
entry_range.copyValuesToRange(master_sheet, 1, 3, master_row, master_row);
}

How to disable editing cells after a particular time in Google Spreadsheet?

Use Case - I have shared a Google Spreadsheet amongst a dozen friends and we are entering our prediction for matches. The catch is to enter it before the game starts.
Using Spreadsheet as everyone can see everyone's prediction.
Problem - Is there an AddOn or any feature which allows to disable editing a few cells after a particular time? Say post midnight A[7]-M[7] cells cannot be edited.
You can set a script to run at a specific time:
In the script editor, create a function:
function protectRangeAtMidnight() {
//My code will go here
}
In the script editor, click on the RESOURCES menu, and choose, CURRENT PROJECTS TRIGGERS.
Add a trigger for a specific date and time.
The problem is, what is the code going to do? If you protect the range, but the people you are sharing the spreadsheet with have edit authority, they can just unprotected the range. If you change their permissions to VIEW only, then you'd have to change it back at some point for the next game. That would work as long as there can be a time period where no one else can edit the sheet.
You can remove a user from the editor list:
Remove Editor
function protectRangeAtMidnight() {
SpreadsheetApp.openById('The SS ID').removeEditor(emailAddress);
}
You can also set file sharing permissions through DriveApp:
setSharing(accessType, permissionType)
I am actually doing exactly the same thing and came across the same problem. The solution I came up with does not lock the cells but uses data validation. Some of the solutions suggested online did not seem to take into account that you need to lock a row of results which have a date associated with it.
This is the layout I am using for my predictions:
Google sheet cropped image example
The cells in blue then have the following data validation (criteria is custom formula, reject input):
=if(isnumber(C1),and(now()<$A1,C1>=0,C1-int(C1)=0))
It checks that what is entered in C1 is a number. If it is, it then checks the following:
If the current date and time is before 'kick off'.
If the number is greater than or equal to zero.
That it is a whole number.
If so, it allows the cell to be changed. If the match has kicked-off, the cell cannot be altered and a red triangle will appear in the cell (because the data validation will be violated as now() will be after the date in question) but it stops the cells from being changed once the game has kicked off.
If you couple the above with locking the entire sheet (apart from the blue cells) it should allow users to make predictions prior to kick-off.
If it is necessary for cells to be altered after the game has begun you can modify the date in column A to then make the update before changing the date back.
Hope this helps!
What works is to:
Lock the cells or columns at a specific time, then remove the protection at another time: using a daily trigger (or even manually)
Modifications needed before running the functions:
Sheets names (In my example it's Sheet1, Sheet2)
Range to protect (in my example it's A:D) (editors won't be able to edit the specified range)
function Lock() {
var tabs = ['Sheet1', 'Sheet2'];
var ss = SpreadsheetApp.getActiveSpreadsheet();
for (var i = 0; i < tabs.length; i++) {
var spreadsheet = ss.getSheetByName(tabs[i]);
var protection = spreadsheet.getRange('A:D').protect();
protection.setDescription('Protected')
protection.removeEditors(protection.getEditors());
}
};
And to remove the protection you'll use the following script: Editors get "back" their permission to edit the specified range:
function Unlock() {
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
if (protections[i].getDescription() == 'Protected') {
protections[i].remove();
}
}
};