Hide / show specific Sheets tabs for specific users on shared file - google-apps-script

My google doc should prompt a login box upon opening the Google Sheets file and, based on the password, it should display a specific tab. E.g. for "password1" it should allow the user to see only tab "Sheet1" and other sheets should be hidden. Similarly, for "password2" it should allow the user to see and work on tab "Sheet2" only.
I tried to run the following code, however it shows some errors.
function showLoginDialog() {
var sheet3 = SpreadsheetApp.getActiveSheet('Sheet3');
sheet3.hideSheet();
var sheet2 = SpreadsheetApp.getActiveSheet('Sheet2');
sheet2.hideSheet();
var sh = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet('Sheet1');
var ui = SpreadsheetApp.getUi();
var prompt = ui.prompt('Password','Enter Password',ui.ButtonSet.OK_CANCEL);
var response = prompt.getResponseText();
var button = prompt.getSelectedButton();
if(button==ui.Button.OK) {
if(response=='pwd3') {
sheet3.activate();
}//end of inner if
if(response=='pwd2'){
sheet2.activate();
}
}//end of main if
}//end of function
The Google Sheets file should ask for the password upon opening it, and it should display the respective sheet based on the password.

You want to open only one of all sheets in the Spreadsheet by the inputted value from the dialog.
For example, when the value of pwd2 is inputted, you want to open only "Sheet2".
You want to open the dialog when the Spreadsheet is opened.
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
The flow of the modified script is as follows.
Open only "Sheet1" as a default, when the Spreadsheet is opened.
Open a dialog.
When pwd2 is inputted, only "Sheet2" is opened.
When pwd3 is inputted, only "Sheet3" is opened.
If you want to open the dialog when the Spreadsheet is opened, please install OnOpen event trigger to RunOnOpen().
How to install OnOpen trigger:
Open the script editor.
Edit -> Current project's triggers.
Click "Add Trigger".
Set RunOnOpen for "Choose which function to run".
Set "From spreadsheet" for "Select event source".
Set "On open" for "Select event type".
Modified script:
function RunOnOpen() {
// Updated
var openSheet = function(ss, sheets, sheet) {
var s = ss.getSheetByName(sheet);
s.showSheet();
ss.setActiveSheet(s);
for (var i = 0; i < sheets.length; i++) {
if (sheets[i].getSheetName() != sheet) {
sheets[i].hideSheet();
}
}
};
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
openSheet(ss, sheets, "Sheet1"); // Open only "Sheet1" when this function is run.
var ui = SpreadsheetApp.getUi();
var prompt = ui.prompt('Password','Enter Password',ui.ButtonSet.OK_CANCEL);
var response = prompt.getResponseText();
var button = prompt.getSelectedButton();
if (button == ui.Button.OK) {
var sheet = "";
if(response == 'pwd3') {
openSheet(ss, sheets, "Sheet3");
} else if(response == 'pwd2') {
openSheet(ss, sheets, "Sheet2");
}
}
}
Note:
In this modified script, when RunOnOpen() is manually run at the script editor, the script works. In order to do this, I didn't use the event object of OnOpen event trigger.
References:
hideSheet()
showSheet()
Installable Triggers
If I misunderstood your question and this was not the result you want, I apologize.
Updated:
When I tested several times for this script, there are the case that "Sheet1" sheet and another sheet are opened. In order to avoid this situation, I updated the script. Could you please confirm above script again?
Edit:
You want to make users use the specific sheet of Spreadsheet by inputting each password.
You don't want to publish the script for users.
You want to make several users use the Spreadsheet, simlutaneously.
If my understanding for your goal is correct, how about this workaround?
Create each Spreadsheet for each user.
By this, the simultaneous access for the Spreadsheet can be achieved.
Use Web Apps in order to open each Spreadsheet by inputting the password.
The flow of this workaround is as follows.
An user, who logged in Google, open Web Apps.
The user input the password and click "OK".
Checking the password at the server side and return the URL of Spreadsheet corresponding to the password.
Open the Spreadsheet from the URL.
By this flow, I think that top 3 aims can be achieved.
Sample script:
Before you use this script, please split the Spreadsheet for each sheet, and retrieve each URL of Spreadsheet.
When you use this, please deploy Web Apps with the following condition. If you want to know how to deploy Web Apps, please check this.
Execute the app as: Me.
Who has access to the app: Anyone
By this condition, users are required to log in to Google for accessing to Web Apps.
Users accesses to the URL of Web Apps.
Google Apps Script: Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("index");
}
function selectSpreadsheet(value) {
var url = "";
if (value == "pwd2") {
url = "### URL of Spreadsheet for password of pwd2 ###";
} else if (value == "pwd3") {
url = "### URL of Spreadsheet for password of pwd3 ###";
}
return url;
}
HTML & Javascript: index.html
<input type="text" id="value" >
<input type="button" value="ok" onclick="openSpreadsheet()">
<script>
function openSpreadsheet() {
var value = document.getElementById("value").value;
google.script.run.withSuccessHandler((url) => {
if (url) window.open(url,'_top');
}).selectSpreadsheet(value);
}
</script>
Note:
When you modified the script, please deploy the project as new version. By this, the latest script is reflected to Web Apps. Please be careful this.
I think that when the each Spreadsheet is shared for only each user, the security will be high.
The maximum number of the simultaneous access for Web Apps is 30. Ref
This is a simple sample script. So if you use this, please modify it for your situation.
From TheMaster's somment, Although it's highly unlikely that users are able to retrieve the passwords from source code, I recommend using private functions and/or properties service for storing of passwords for a extra layer of security.
As a sample, you can use like below by saving the passwords and URLs to PropertiesService.
function selectSpreadsheet(value) {
return PropertiesService.getUserProperties().getProperty(value);
}
References:
Class google.script.run
Web Apps
Taking advantage of Web Apps with Google Apps Script

I don't think this is secure for the following reasons:
Sheets' hidden/visible property is global and not restricted to a single user. If user1 and user2 login one after another at almost the same time, user1's sheet is visible after user1's login; As soon as user2 logs in, user1's sheet will be hidden for both users and user2's sheet will be visible to both user1 and user2
It's easy to unhide sheets. user1 can unhide user2's sheet at any time he wishes to, provided he has edit access to the spreadsheet(which is needed, if you want to display the login dialog).

Related

Giving access to a Google Spreadsheet and its accompanying Apps Script

I have a spreadsheet I'm using to manage a bunch of content, with a script I've written that adds an "Export" button to the menu. When the button is clicked, the script gets all the appropriate data and formats it all in a specific way. The formatted version is saved to my Google Drive with a timestamp but a download link is also provided. I'll include a simplified version of the script below in case modifications are required.
I rarely ever use Google's Apps Scripts so I'm rather unfamiliar with the ins and outs of it. I only know the basics (how to write a script that can run when something is done from the spreadsheet's page).
I'm aware I can invite a user to my spreadsheet (or just make it public) but that doesn't seem to bring the script along with it. The script and all the formatting that's being done is the main part of what the person I'm inviting needs. I'm aware that for file.getDownloadUrl() to work (assuming the file is still saving on my Drive), I'd need to give the individual access to that folder as well which isn't a problem.
The question is, how do I give them access to the script so they get the Export menu item? Am I not able to? Am I basically limited to creating a button with the export function bound to it?
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var csvMenuEntries = [
{
name: "Export as CSV",
functionName: "csvExport"
},
{
name: "Export for wiki",
functionName: "wikiExport"
}
]
ss.addMenu("Export", csvMenuEntries)
}
function prepare(type) {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const ssName = ss.getName()
const sheet = ss.getSheets()[0]
const sheetName = sheet.getSheetName()
const folderName = ssName + ' exports'
let folder
try {
folder = DriveApp.getFoldersByName(folderName).next()
} catch (err) {
folder = DriveApp.createFolder(folderName)
}
let fileName
if (type) {
const extension = type === 'csv' ? 'csv' : 'txt'
fileName = ssName + '_' + sheetName + `_${type}_` + new Date().getTime() + `.${extension}`
}
return { ss, ssName, sheet, sheetName, folder, fileName }
}
function download(file) {
const downloadURL = file.getDownloadUrl().slice(0, -8)
showUrl(downloadURL)
}
function showUrl(downloadURL) {
var link = HtmlService.createHtmlOutput(`Click here to download`)
SpreadsheetApp.getUi().showModalDialog(link, 'Your file is ready!')
}
function csvExport() {
const { ss, sheet, folder, fileName } = prepare('csv')
const csvSettings = getCsvSettings(ss)
const csvFile = convertRangeToCsv(sheet, csvSettings) // not going to share this. It's simple but irrelevant
const file = folder.createFile(fileName, csvFile)
download(file)
}
function wikiExport() {
const { sheet, folder, fileName } = prepare('wiki')
const wikiFile = convertRangeToWikiFormat(sheet) // not going to share this. It's simple but irrelevant
const file = folder.createFile(fileName, wikiFile)
download(file)
}
A container-bound script has the same access as its parent spreadsheet, so if you're sharing the spreadsheet you're also sharing the script (though if they have only view access they have to create their own copy to see it):
All container-bound scripts use the same owner, viewer, and editor access list defined for the container file.
With that in mind, there are a few limitations when using scripts. First, they will not trigger for anonymous users (i.e., users that are not signed in), even if the sheet is editable to the public. You'll notice that if you try to open the script editor as anonymous, you will be asked to sign in. There's also a feature request to allow this on Google's issue tracker here.
Secondly, even if the users are signed in, there are other restrictions for Apps Script's triggers:
onOpen(e) runs when a user opens a spreadsheet, document, presentation, or form that the user has permission to edit.
Users need permission to edit the file for the onOpen() trigger to run. If they have viewer or commenter access the menu won't show up. In fact, you'll find that most script functions won't work if the users have only viewer access since they need editor access for most interactions with the sheet.
So if you want this menu to show up you'll need to give your users explicit editor access. If you really must keep your sheet as view-only or want to interact with anonymous users you can consider building a Web App instead and have the users get the download link from there. The web app has ways to communicate with the back-end or the Sheet so you should be able to reproduce your current code that way as well.
References:
Web Apps
Communicating with server functions
Triggers

Using triggers with custom functions on Google Apps Script

I had a prob with my script, which was greatly answered in this question.
Basically custom functions cannot call services that require authorization. However, as far as I understood if I use simple triggers, such as onEdit it could work.
I checked the documentation suggested in the previous question, however I wasn't successful applying that to my code, which you can see below:
function FileName (id) {
var ss = DriveApp.getFileById(id);
return ss.getName();
}
How could I adapt my code to use simple triggers?
Here is a sample sheet that replicates the problem.
I believe your goal as follows.
You want to use your function of FileName as the custom function of Google Spreadsheet.
You want to automatically retrieve the filename when the file ID is put to the column "B".
You want to put the filename to the column "C".
Issue and workaround:
Unfortunately, when the custom function is used, in the current specification, the most methods except several methods (for example, one of them is UrlFetchApp.) that the authorization is required cannot be used. By this, DriveApp.getFileById(id) in your script cannot be used with the custom function. But there is a workaround. At the custom function, UrlFetchApp can be used. In this answer, I would like to propose to use the Web Apps with UrlFetchApp as the wrapper for authorizing. By this, the authorization can be done with the Web Apps. So your function can be run by the custom function.
Usage:
1. Prepare script.
Please copy and paste the following script to the script editor and save it.
const key = "samplekey"; // This is a key for using Web Apps. You can freely modify this.
// This is your function.
function FileName_(id) {
var ss = DriveApp.getFileById(id);
return ss.getName();
}
// Web Apps using as the wrapper for authorizing.
function doGet(e) {
let res = "";
if (e.parameter.key === key) {
try {
res = FileName_(e.parameter.id);
} catch (err) {
res = `Error: ${err.message}`;
}
} else {
res = "Key error.";
}
return ContentService.createTextOutput(JSON.stringify({value: res}));
}
function Filename(id) {
const webAppsUrl = "https://script.google.com/macros/s/###/exec"; // Please set the URL of Web Apps after you set the Web Apps.
const res = UrlFetchApp.fetch(`${webAppsUrl}?id=${id}&key=${key}`);
if (res.getResponseCode() != 200) throw new Error(res.getContentText());
return JSON.parse(res.getContentText()).value;
}
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Anyone, even anonymous" for "Who has access to the app:".
In this case, the access token is not required to request to Web Apps. But in this sample script, a key for requesting to Web Apps is used.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
Copy the URL of Web Apps. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.
Please set the URL of https://script.google.com/macros/s/###/exec to url of above script. And please redeploy Web Apps. By this, the latest script is reflected to the Web Apps. So please be careful this.
3. Test this workaround.
When the file ID is put to the cell "A1", please put =filename(A1) to a cell as the custom function. By this, the script is run and the response value is returned.
Note:
Above sample script is a simple sample script for testing your script. So when you want to use the various methods, this post might be useful.
Please use this script with enabling V8.
As other method, I think that when the file ID is manually put to the column "B", the installable OnEdit trigger can be used. The sample script is as follows. Please set the sheet name. And please install the trigger to the function of installedOnEdit. Ref By this, when the file ID is put to the column "B" of sheetName, the file ID is put to the column "C".
function installedOnEdit(e) {
const sheetName = "Sheet1";
const range = e.range;
const sheet = range.getSheet();
if (!(sheet.getSheetName() == sheetName && range.getColumn() == 2 && range.getRow() > 1)) return;
const value = range.getValue();
let res = "";
try {
res = DriveApp.getFileById(value).getName();
} catch(e) {
res = e.message;
}
range.offset(0, 1).setValue(res);
}
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Enhanced Custom Function for Google Spreadsheet using Web Apps as Wrapper
Related questions
Can you write a Google Sheets function that draws something?
Error when running Youtube Data Service in App Scripts (js) – Daily Limit for Unauthenticated Use Exceeded
How to enable not authorized users to protect the spreadsheet
Changing Owner of the Sheet irrespective of the duplicator
Installable Triggers
As you can draw from the documentation, simple triggers cannot access services that require authorization neither
You have to use installable triggers instead.
However the workflow is very different from custom functions.
In your specific case, you can implement e.g. that when a cell in column A is being edited (that is a new URL is being inserted) - the respective file name is being found and returned into column D.
You can retrieve the value and the row in which the new URL is being inserted with help of event objects.
Sample:
function FileName (event) {
var id = event.value;
var ss = DriveApp.getFileById(id);
var row = event.range.getRow();
var sheet = event.range.getSheet();
// for column D:
var column = 4;
var returnCell = sheet.getRange(row,column);
returnCell.setValue(ss.getName());
}
For using an installable onEdit trigger - bind it to this function through going on Edit > Current project's triggers as explained here.

Changing Owner of the Sheet irrespective of the duplicator

I have a spreadsheet which contains a sheet that can be duplicated using a script by various users. The problem is when the user duplicates the sheet he becomes the default owner and gets the rights to edit even the protected ranges for that sheet.
My current script copies the protection to new sheet perfectly but the duplicator user becomes the editor for the same.
Please help me in how can a user who is allowed to duplicate the sheet but does not become editor of the protected ranges or a method to reset the owner of the sheet (not the spreadsheet) back to admin user.
My current code:
function Protect() {
var spreadsheet = SpreadsheetApp.getActive();
var myValue = SpreadsheetApp.getActiveSheet().getSheetName();
spreadsheet.duplicateActiveSheet();
var totalSheets = countSheets(); //script function
myValue = "DO" + totalSheets;
SpreadsheetApp.getActiveSpreadsheet().renameActiveSheet(myValue);
var protection = spreadsheet.getActiveSheet().protect();
protection.setUnprotectedRanges([spreadsheet.getRange('C2:E5'), spreadsheet.getRange('C6:D7'), spreadsheet.getRange('F5:G6'), spreadsheet.getRange('B9:G18'), spreadsheet.getRange('G7:G8')])
.removeEditors(['user1.com', 'user2.com', 'user3.com']);
spreadsheet.getRange('G2').setValue(myValue);
spreadsheet.getRange('G3').setValue(new Date()).setNumberFormat('dd-MMM-YYYY');
spreadsheet.getRange('H1:H').clearContent();
};
I believe your goal as follows.
When the script of Protect() is run by an user who is not the owner, you want to permit to copy the sheet and don't want to add the user as the editor to the copied whole sheet using Google Apps Script.
The Spreadsheet has already been shared with the users.
Modification points:
In this case, I thought that to run the script by the owner might be the solution of your issue, when the user runs the script.
When the user is run the script, in order to run the script by the owner who is not the user, I would like to propose to use Web Apps.
Usage:
1. Prepare script.
Please copy and paste the following script to the script editor and save it.
function doGet() {
script();
return ContentService.createTextOutput();
}
function Protect() {
const url = ScriptApp.getService().getUrl();
UrlFetchApp.fetch(url, {headers: {authorization: "Bearer " + ScriptApp.getOAuthToken()}});
// DriveApp.getFiles() // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive.readonly". This scope is used for the access token.
}
// This script is the same with your "Protect".
function script() {
var spreadsheet = SpreadsheetApp.getActive();
var myValue = SpreadsheetApp.getActiveSheet().getSheetName();
spreadsheet.duplicateActiveSheet();
var totalSheets = countSheets(); //script function
myValue = "DO" + totalSheets;
SpreadsheetApp.getActiveSpreadsheet().renameActiveSheet(myValue);
var protection = spreadsheet.getActiveSheet().protect();
protection.setUnprotectedRanges([spreadsheet.getRange('C2:E5'), spreadsheet.getRange('C6:D7'), spreadsheet.getRange('F5:G6'), spreadsheet.getRange('B9:G18'), spreadsheet.getRange('G7:G8')])
.removeEditors(['user1.com', 'user2.com', 'user3.com']);
spreadsheet.getRange('G2').setValue(myValue);
spreadsheet.getRange('G3').setValue(new Date()).setNumberFormat('dd-MMM-YYYY');
spreadsheet.getRange('H1:H').clearContent();
};
In this script, countSheets() is not included. Because I'm not sure about countSheets() from your question. So please be careful this. Please prepare this.
2. Deploy Web Apps.
On the script editor, Open a dialog box by "Publish" -> "Deploy as web app".
Select "Me" for "Execute the app as:".
By this, the script is run as the owner.
Select "Anyone" for "Who has access to the app:".
In this case, the access token is required to request to Web Apps.
Click "Deploy" button as new "Project version".
Automatically open a dialog box of "Authorization required".
Click "Review Permissions".
Select own account.
Click "Advanced" at "This app isn't verified".
Click "Go to ### project name ###(unsafe)"
Click "Allow" button.
Click "OK".
3. Test this workaround.
Please click the button assigned with Protect. By this, the script is run by the owner. By this, even when the user is clicked the button, the result of script is the same with that run by the owner.
Note:
Please use this script with enabling V8.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script

My Script works for some people but not others

I created an audit template in Google sheets that uses several scripts. The script is designed to populate a cell with the active user's email and a time date stamp when they click on a check box. The script works, except for one person, the script does not recognize his active user email. Can someone tell me why the script works for some but not others and how I might fix it?
I've tested the script in a blank test spreadsheet and it works for this person. We had this same issue in another Google Sheet, but we remedied it my creating a new spreadsheet and copying all of the tabs and script. However, attempts to do the same with this spreadsheet have failed.
Here's my script:
function onEdit(e) { //this is the event Google Sheets fires when cells are changed
var email = Session.getActiveUser().getEmail(); // Get the email address of the person running the script.
var date = new Date();
var spreadsheet = SpreadsheetApp.getActive();
if (e.source.getActiveSheet().getName() == "InterimTestsOfControls") { //only do this if the changed cell is on the specific worksheet you care about
switch (e.range.getA1Notation()) { //This gets the cell that was edited
case "E5": //and this switch statement allows you to only respond to the cells you care about
if (e.value == "TRUE") {
SpreadsheetApp.getActiveSheet().getRange("F5").setValue("Prepared by " + email + " " + date);
Logger.log(email);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Audit Planning'), true).getRange('C7').activate().setValue(email);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('InterimTestsOfControls'), true).getRange('C5').activate();
}
else {
SpreadsheetApp.getActiveSheet().getRange("F5").setValue("Click box to sign-off as preparer");
Logger.log(email);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Audit Planning'), true).getRange('C7').activate().setValue("");
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('InterimTestsOfControls'), true).getRange('C5').activate();
}
}
}
}
From the official Session class documentation:
getActiveUser()
Gets information about the current user. If security policies do not allow access to the user's identity, User.getEmail() returns a blank string.
This situation may happen under many, different circumstances:
The user's email address is not available in any context that allows a script to run without that user's authorisation, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user).
These restrictions generally do not apply if the developer runs the script themselves or belongs to the same G Suite domain as the user.
In your situation I see that you are using a simple onEdit(e) trigger - so it looks like this may be the issue. You may want to check out Installable triggers instead.
function onEdit(e) {
var sh=e.range.getSheet();
var email = Session.getActiveUser().getEmail(); //This is a permissions issue. You may have to use an installable trigger and even then it's not guaranteed
var date = new Date();//you might want to use Utilities.formatDate()
//var spreadsheet = SpreadsheetApp.getActive();this is than same as e.source
if (sh.getName()=="InterimTestsOfControls") {
//you can selection your cells with information that's already in the event block rather than having to run another function
if(e.range.columnStart==6 && e.range.rowStart==5) {
if (e.value=="TRUE") {
sh.getRange("F5").setValue("Prepared by " + email + " " + date);
e.source.getSheetByName('Audit Planning').getRange('C7').setValue(email);
} else {
e.source.getRange("F5").setValue("Click box to sign-off as preparer");
e.source.getSheetByName('Audit Planning').getRange('C7').setValue("");
}
}
}
}
Getting and setting the active range is a remnant of the Macro Recorder following your every step and in general is not required in your scripts. And in fact it will slow down your scripts so please try to avoid using that approach in general for most scripting.

Google Sheets Auto-Populate the author for each row on change of column B

How can I add an auto-populate of the user who last edited the row to the following code that auto-populates the date the column was last edited. Here is the current script:
function onEdit(e) {
var s = e.source.getActiveSheet(),
cols = [2],
colStamp = 1,
ind = cols.indexOf(e.range.columnStart)
if (s.getName() !== 'Log' || ind == -1) return;
e.range.offset(0, parseInt(colStamp - cols[ind]))
.setValue(e.value ? new Date() : null);
}
So, ultimately... I would want to add the user name to column G on edit of each row. Any ideas?
You can retrieve user name using Class Session. The detail information is here. https://developers.google.com/apps-script/reference/base/session
Please add following script to last row of your script. The user name is imported to column G. If you want user e-mail, please change from getUsername() to getEmail().
Script :
s.getRange(e.range.getRow(), 7).setValue(Session.getEffectiveUser().getUsername());
About onEdit()
If the onEdit() is not installed as a trigger, other shared users cannot use Session.getEffectiveUser().getUsername() because of authMode=LIMITED. So the user name cannot be retrieved. By installing onEdit() as a trigger, authMode becomes FULL. So you can retrieve user data using Session.getEffectiveUser().getUsername().
The detail information for installing trigger is https://developers.google.com/apps-script/guides/triggers/installable#managing_triggers_manually
If I misunderstand your question, I'm sorry.
Added 1 :
In order to retrieve user information, the user has to install a trigger for onEdit(). I had forgotten about this. I'm sorry.
For example, owner of spreadsheet and user with a permission for editing are OWNER and USER, respectively. When OWNER installs a trigger for onEdit(), the user of spreadsheet becomes OWNER. At this time, when USER edits the spreadsheet, the user name becomes OWNER.
When I have worked a test, I have installed as USER. So I had thought that it works. But it was wrong. So I thought for the solution as follows.
Install a trigger for onEdit() using onOpen().
This didn't work, as you know.
Display a dialog box and a button using onOpen(). Install a trigger for onEdit() by the button.
This didn't work, because of existing several triggers for each user.
Display a dialog box and a button using onOpen(). while temporarily install a trigger for onEdit(), retrieve the user name and put it to cache by the button. The trigger is removed after retrieved user name soon.
This works fine.
I propose the 3rd method. In this script, it is not necessary to install triggers manually. If you want to change the hold time of cache, please modify cache.put().
Script :
function getUser() {
var triggerId = ScriptApp.newTrigger('onEdit')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create().getUniqueId();
var user = Session.getEffectiveUser().getUsername();
var triggers = ScriptApp.getProjectTriggers();
for (i in triggers) {
if (triggers[i].getUniqueId() == triggerId) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
var cache = CacheService.getUserCache();
cache.put("username", user, 3600); // For example, hold user name for 1 h
}
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().show(
HtmlService
.createHtmlOutput('<input type="button" value="OK" onclick="google.script.run.withSuccessHandler(function(){google.script.host.close()}).getUser()">')
.setTitle('Push OK button.')
.setWidth(400)
.setHeight(100)
);
}
function onEdit(e) {
var cache = CacheService.getUserCache();
var user = cache.get("username"); // Please use this as user name.
}
Flow of script :
When spreadsheet is opened, a dialog is opened on the spreadsheet by onOpen().
When user pushes "ok", the user name is retrieved and put to the cache by getUser().
When user edits the spreadsheet, the user name is retrieved from the cache by onEdit().
Please copy and paste this script. You can use user in onEdit().
When I have been confirming this again, I noticed that in order to use this script, each user has to be authorized. The authorization is https://developers.google.com/apps-script/guides/services/authorization
Added 2 :
I report a solution for retrieving shared user information at spreadsheet. It was found as follows.
User information retrieving by Class Session is the owner and users which installed triggers by themselves.
When each user installs a trigger, user information retrieving by Class Session losts the accuracy. So user information has to be retrieved using a temporally installed trigger.
Using onOpen(), it cannot directly install triggers and authorize.
Using menu bar, it can install triggers and authorize Google Services using API.
Here, I thought 2 problems.
The confirmation whether the authorization was done.
At onOpen(), although many methods using Google API can be executed without the authorization, there are also some methods which cannot be executed without the authorization. Furthermore, there are some methods which cannot execute even if the authorization was done. It's trigger. On the other hand, DriveApp requires the authorization for only the first time, but it can use without the authorization after 2nd times.
I thought that users can find easily by displaying information in a dialog box when spreadsheet is launched. So I adopted displaying information using the dialog box. But, there is a big limitation for the dialog box.
Using a click of button on a dialog box, it can install triggers. However it cannot authorize Google Services using API.
Using above information, I thought a flow to retrieve user information.
When user opens the spreadsheet for the first time, it displays 'Please authorize at "Authorization" of menu bar.' using a dialog box, and creates a menu bar "Authorization".
The user clicks "OK" button on the dialog box and run "Authorization" at the menu bar. By running "Authorization", the user information is retrieved by a temporally installed trigger.
When the user opens the spreadsheet after the 2nd time, the authorization is checked by DriveApp. A dialog box with 'Push OK button.' is displayed. By clicking "OK", the user information is retrieved by a temporally installed trigger.
By this flow, the user information which is using the shared spreadsheet can be retrieved. Although I think that there may be also other solutions, I proposal this as one of solutions.
Script :
function getUser(){
var triggerId = ScriptApp.newTrigger('getUser')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create()
.getUniqueId();
var userInf = Session.getEffectiveUser();
var userName = userInf.getUsername();
var userMail = userInf.getEmail();
var triggers = ScriptApp.getProjectTriggers();
[ScriptApp.deleteTrigger(i) for each (i in triggers) if (i.getUniqueId() == triggerId)];
CacheService.getUserCache().putAll({
"username": userName,
"usermail": userMail
}, 3600);
}
function dialogForGetUser(){
SpreadsheetApp.getActiveSpreadsheet().show(
HtmlService
.createHtmlOutput('<input type="button"\
value="OK"\
onclick="google.script.run.withSuccessHandler(function(){google.script.host.close()})\
.getUser()">'
)
.setTitle('Push OK button.')
.setWidth(400)
.setHeight(100)
);
}
function dialogForAuth(){
SpreadsheetApp.getActiveSpreadsheet().show(
HtmlService
.createHtmlOutput('<input type="button"\
value="OK"\
onclick="google.script.host.close()">'
)
.setTitle('Please authorize at "Authorization" of menu bar.')
.setWidth(400)
.setHeight(100)
);
}
function getAuth() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.removeMenu("Authorization");
getUser();
ss.toast("Done.", "Authorization", 3);
}
function onOpen(){
try {
var temp = DriveApp.getFileById(SpreadsheetApp.getActiveSpreadsheet().getId());
dialogForGetUser();
} catch(e) {
dialogForAuth();
SpreadsheetApp.getActiveSpreadsheet().addMenu(
"Authorization",
[{
functionName:"getAuth",
name:"Run this only when the first time"
}]
);
}
}
function onEdit(e){
var cache = CacheService.getUserCache();
var user = cache.getAll(["username", "usermail"]);
// user.username is user name.
}
When the spreadsheet is opened, at first, onOpen() is executed. It is checked whether the user has already authorized.
If the user has never authorized yet, dialogForAuth() is executed. If the user has already authorized. dialogForGetUser() is executed.
In this case, you can retrieve user name by user.username at onEdit().