I have created a spreadsheet with all sheets protected from editing except for a single cell, where the user is supposed to enter a search value, which filters the sheet. I shared the spreadsheet with enabled editing to allow for that, but that also enables users to create new sheets, which I'd like to prevent.
Perhaps I'm using not the most optimal way of achieving what I want, so suggestions are welcome. I saw people advising to use forms in similar use cases, but I don't see how to apply their survey capabilities to my needs.
EDIt: here is the shared spreadsheet: https://docs.google.com/spreadsheets/d/1C4mrBWJxPLrFQ4bp82UA2ICOr1e6ER47wF7YuElyoZg/edit?usp=sharing
You can use a script to delete every newly created (not by the owner) sheet. Here is an example of such a script :
// Deletes any tab named "SheetN" where N is a number
function DeleteNewSheets() {
var newSheetName = /^Sheet[\d]+$/
var ssdoc = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ssdoc.getSheets();
// is the change made by the owner ?
if (Session.getActiveUser().getEmail() == ssdoc.getOwner().getEmail()) {
return;
}
// if not the owner, delete all unauthorised sheets
for (var i = 0; i < sheets.length; i++) {
if (newSheetName.test(sheets[i].getName())) {
ssdoc.deleteSheet(sheets[i])
}
}
}
Then set a trigger that runs this function DeleteNewSheets when a change (like sheet creation) happens.
Note : If english is not the document language you should change the regular expression /^Sheet[\d]+$/, for example for french it should be /^Feuille [\d]+$/.
Without seeing your existing spreadsheet, Seedmanc, Google's "Filter views" option under the "Data" menu of your Sheet could be helpful, because it lets owners/editors create and save filters that are accessible to view-only users too.
What's more, view-only users can create filter views unique to them. Once they enter a filter range, menu arrows appear...
...allowing them to filter either by condition (including custom formulas!) or by values. So, depending on how elaborate your filter is and how spreadsheet-savvy your users are, filter views can provide a more flexible filter while still protecting your sheets.
(Read more about Google Sheets filter views here.)
Related
Hi and thanks for reading. I'm trying to make a system where responders can enter the names of participants in one form. In another form, for entering scores of a competition, responders can select the name in a dropdown menu. I'm not a programmer, but I got quite far by Googling and 'stealing' code. What I have now, is a code in the first form (for adding names). When you submit it, the form adds the name to a Google sheet. Then the code adds the names in the Google sheet to the form for entering scores. So far so good!
But now the problem: there is more than one competition. What I could do, is change the code so that is also adds the names in the Google sheet to the form of the other competition, but that's a lot of work everytime there is a new competition. It would be easier to have a code in the competitionform itself that tells the form: every time someone opens you (as in: a responder, not an editor), run this code. Is that possible? I now use the 'on submit' trigger for the other form, but I actually need an 'on entry' trigger or something like that. I tried 'on open' but that only works when the editor opens the form.
Here is the code I have in the 'add name' form (the trigger is located somewhere else)
function addRider(){
// call your form and connect to the drop-down item
var form = FormApp.openById("1NeKicU9d3yqQX1D2ZliE-t87k3OVW8lqn9avhNs54NM");
var riderList = form.getItemById("10469776").asListItem();
// identify the sheet where the data resides needed to populate the drop-down
var rider = SpreadsheetApp.openById("1kLBqtU4ebR3tElrEIZNklmxr0hu5IwE6_UV7AN9T1wE").getSheetByName("Riders");
// grab the values in the first column of the sheet - use 2 to skip header row
var riderValues = rider.getRange(2, 3, rider.getMaxRows() - 1).getValues();
var riderNames = [];
// convert the array ignoring empty cells
for(var i = 0; i < riderValues.length; i++)
if(riderValues[i][0] != "")
riderNames[i] = riderValues[i][0];
// populate the drop-down with the array data
riderList.setChoiceValues(riderNames);
}
I am working on a large Google Sheet that has many editors from multiple companies, so I have had to give permissions on each tab on a per column basis. The issue is, because most editors only have editor access to 10-20% of the columns, the "Create New Filter View" feature is not working for those users. I am finding Filter View access to be very finicky in general. Sometimes it works, sometimes it doesn't on a sheet with similar protections. Other times "Create New Filter View" is not an available option, but if you select an existing filter view and click "Duplicate" you can create one. Seems like there are a few weird bugs with Google Sheets and permissions that I can't pin down.
Any ideas on how to use Apps Script to unlock the protections on the "Create New Filter View" option? This could be for all editors or all viewers, either would work in this instance. Thanks!
Issue:
While a FilterView can be created in a script if the Advanced Sheets Service is enabled, the same issue you're experiencing while creating it in the UI would show up here: a user cannot create a FilterView if one of the columns is protected from this user.
Naturally, this same user cannot run a script to unprotect the corresponding columns either - what worth would a protection be if the users to which it applies could remove it!
Possible workarounds:
A way around this would be to have a script that runs under the authority -see getEffectiveUser()- of a user who can edit those protected columns. In most situations, this is the user that is triggering the script -see getActiveUser()-, but in certain situations, like an installable trigger or a web app that executes as the user who deployed it, that is not the case.
For example, you could install an onEdit trigger with a user who has access to all the columns in the desired FilterView.
Then, this would fire whenever any user edits the spreadsheet, but it would fire under the authority of the user who installed the trigger (and can access the protected columns), so the FilterView could be created.
In order to create the FilterView only for certain edits (for example, when a specific cell is edited and there is a specific edited value), you could check these conditions at the beggining of the onEdit function. And if you needed to pass more information for customization of the filter view (for example, which columns and rows, which sheet, etc.), you could put that information in other cells and retrieve the corresponding values via getValue()/getValues().
For example, It could be something like this:
function fireOnEdit(e) {
if (e.range.getA1Notation() === "E1" && e.value === "Create FilterView") {
const ss = e.source;
const spreadsheetId = ss.getId();
const sheet = ss.getSheets()[0];
const resource = {
requests: {
addFilterView: {
filter: {
range: {
sheetId: sheet.getSheetId(),
startRowIndex: 0,
endRowIndex: 4,
startColumnIndex: 0,
endColumnIndex: 3
}
}
}
}
}
Sheets.Spreadsheets.batchUpdate(resource, spreadsheetId);
}
}
Google Sheets allows to specify (hyper)links in two ways:
By using HYPERLINK formula/function, e.g. =HYPERLINK("http://example.com/", "Example.com")
By using "linking" feature – Insert » Insert Link
There are a lot of solutions around the web, and StackOverflow, for extracting URL from the first option - the HYPERLINK formula, but I haven't found any way how to extract it from the second option.
Example Sheet
How to extract with Apps Script a link URL inserted with Insert » Insert Link
Apps Script has a class https://developers.google.com/apps-script/reference/spreadsheet/rich-text-value that allows you to retrieve not only the plain text contained in a cell, but also its properties, such as the link URL.
To access this property use the method getRichTextValue() combined with getLinkUrl()
Sample to retrieve the link URL in a custom function:
function getLink(range){
var link = SpreadsheetApp.getActive().getActiveSheet().getRange(range).getRichTextValue().getLinkUrl();
return link;
}
After writing and saving this code, you simply need to call it from any cell, giving it the reference of the cell with the link URL as parameter.
Important:
Since it is the reference and not the value of the cell that should be passed to the function, you need to put the A1 notation in quotes.
Sample:
=getLink("A1")
We can improve ziganotschka's solution to avoid the need for quotation marks.
Enter this in B1 (assuming text with the embedded link is in A1):
=getLink(ADDRESS(row(),column(A1)))
This way it can be dragged down to quickly get data for an entire column.
If want to use like normal formula without quotation marks,
function GetURL(input) {
var formula = SpreadsheetApp.getActiveRange().getFormula();
var rx = /=geturl\((.*)\)/gi;
var rng_txt = rx.exec(formula)[1]
var rng = SpreadsheetApp.getActiveSheet().getRange(rng_txt);
return rng.getRichTextValue().getLinkUrl()
}
I want to conditionally format a Google Sheet for specific users. Basically, Teacher#1's edits would appear yellow while Teacher#2's are green. Unfortunately the "conditional formatting" tool in Sheets doesn't have this option. Protecting certain cells doesn't work because of the regularity of changes in editors.. I simply want to see editors, not restrict them. Another suggestion should be to use the "Revision History" option.. but this doesn't seem practical for finding a certain editor's changes over several months using several sheets (that several editors have access to).
So the direction I was thinking of going was having a function with an OnEdit trigger that would grab the email (all users must login) and then highlight the last edit.
Here is the URL for a public Google Sheet I duplicated. There, you'll see one of many Weeks sheets. Then there is a control panel with colors that would be associated with each user (teacher).
I copied some script from another StackOverflow forum to get started.
function setActiveUser() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
// GET EMAIL ADDRESS OF ACTIVE USER
var email = Session.getActiveUser().getEmail();
Logger.log(email);
//HIGHLIGHT LAST EDITED CELL BASED ON OF ACTIVE USER'S PREDETERMINED COLOR
sheet.getRange(here is where I need to locate last edited cell).setBackgroundColor("here is where I need to color it based on the "Control Panel" sheet colors");
Hopefully someday Google will allow this kind of Conditional Formatting, but in the meantime it will help my school in Latin America a ton.
Thank you for considering!
The first line of your code should be:
function onEdit() {
And the last lines should be something like this: (edit)
//HIGHLIGHT LAST EDITED CELL BASED ON OF ACTIVE USER'S PREDETERMINED COLOR
var teacherName = ss.getRangeByName("TeacherName").getValues().map(function(array) { return array[0]; });
var teacherColor = ss.getRangeByName("TeacherColor").getValues().map(function(array) { return array[0]; });
var nameIdx = teacherName.indexOf(email);
if(nameIdx > -1)
sheet.getActiveCell().setBackground(teacherColor[nameIdx]);
To get it working, you should first run the code in the Script Editor and authorize it. And TeacherName and TeacherColor should be set to named ranges beforehand. Test sheet is here.
I am am writing a script in Google Sheets that selectively locks a large block of cells, and each day will need to include one more row of cells into that same protected range.
The script is working so far, in terms of locating the rows that need protection and creating a new protected range each time it is run -- however every time it is run (which I want to be onOpen) it creates another protected range, and another, and another. I've named the protected range "History" but I cannot figure out a way to, upon the next run, unprotect just that range and then protect a new range.
There are other protected cells in the sheet, so I cannot simply remove all protections and then proceed - I need to be able to delete just this one, specifically, and go from there. Since the range of cells being locked is going to keep changing, I need to be able to have the script look for this one range specifically.
Any ideas are appreciated!
To update an existing protection, you first have to "get" it. I can't find an easy way to do this directly, but you can get all of the protections on the sheet (or in the entire spreadsheet) and loop through them to find the one you want.
var protections = ws.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var p in protections) {
if (protections[p].getDescription() == 'History') {
var protection = protections[p];
break;
}
}
Now you can use the protection.setRange method to update its range without creating a new protection:
var rangeNew = ws.getRange('A1:C3');
protection.setRange(rangeNew);
Here's the documentation for the sheet getProtectionsType and protection setRange methods. Note that it could be even better to do all this with named ranges, which you can get and update in the same way as protections.