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.
Related
function onEdit() {
var sheet = SpreadsheetApp.getActive().getActiveSheet();
var xCol = sheet.getActiveRange().getColumn();
var xRow = sheet.getActiveRange().getRow();
if ( xCol == 2 && sheet.getName() =='Çeviri' ) {
var user = Session.getActiveUser().getUsername();
sheet.getRange(xRow, 7).setValue(user);
}
}
This script's purpose is; in Google Docs Excel, when a user change a cell in column B in sheet "Çeviri", it writes that users name 7 horizontal cells away.
It's working though, in some way.
When document's owner changes a cell in column B, app sees it and runs. But when another user changes a cell in column B, app doesn't see it and doesn't run.
I think it's about Google, I guess they changed policies and because of that I can't get any other users name with this script
https://developers.google.com/apps-script/reference/base/session#getuser
So my question is, is there any way to write this app without "getUser" command? Because I think it's the problem.
In the general case, Google no longer allows scripts that run in an onEdit() context to find the identity of the user at the keyboard. From the documentation:
the user's email address is not available in any context that allows a script to run without that user's authorization, 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). However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same G Workspace domain as the user.
In other words, if you are in a Google Workspace domain, you can discover the identity of the user at the keyboard and put their email address in a column.
It is best to ask users to authorize the script before running, which can be done with a custom menu item. See the insertActiveUserEmailAddressInColumn_ script for sample code.
Try using it this way
function onMyEdit(e) {
var sh = e.range.getSheet();
if (e.range.columnStart == 2 && sh.getName() == 'Çeviri') {
var user = Session.getActiveUser().getUsername();
sh.getRange(e.range.rowStart, 7).setValue(user);
}
}
function creattrigger() {
if (ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() == "onMyEdit").length == 0) {
ScriptApp.newTrigger("onMyEdit").forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();
}
}
Run the create trigger function. It's setup so that you can't create more than one.
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).
I'm using IFTTT to update my Google Sheets when I receive a SMS. Now, I would like to take a step ahead and write a Google Apps Script that would do different things with the data updated by IFTTT into my Google Sheet. I tried to achieve the same using the Google Apps Script's onEdit function, but that does not work.
I did a lot of search on multiple forums regarding this problem, and I learnt that onEdit works only when a "user" makes the changes to the Google Sheet and not when the changes are done over an API request (I believe IFTTT uses the same). I could not see even a single post with a working solution.
Any ideas? Thanks!
After a lot of Google search, I found below code to be working for me. It is inspired by this answer by Mogsdad.
function myOnEdit(e) {
if (!e) throw new Error( "Event object required. Test using test_onEdit()" );
// e.value is only available if a single cell was edited
if (e.hasOwnProperty("value")) {
var cells = [[e.value]];
}
else {
cells = e.range.getValues();
}
row = cells[cells.length - 1];
// Do anything with the row data here
}
function test_onEdit() {
var fakeEvent = {};
fakeEvent.authMode = ScriptApp.AuthMode.LIMITED;
fakeEvent.user = "hello#example.com";
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
// e.value is only available if a single cell was edited
if (fakeEvent.range.getNumRows() === 1 && fakeEvent.range.getNumColumns() === 1) {
fakeEvent.value = fakeEvent.range.getValue();
}
onEdit(fakeEvent);
}
// Installable trigger to handle change or timed events
// Something may or may not have changed, but we won't know exactly what
function playCatchUp(e) {
// Build a fake event to pass to myOnEdit()
var fakeEvent = {};
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
myOnEdit(fakeEvent);
}
Hope this helps someone in future. Do note that the functions playCatchUp and myOnEdit must be set as "change" and "edit" action triggers respectively in Google Apps Script.
I modified a script provided from this blog
How to have your spreadsheet automatically send out an email when a cell value changes
After some debugging an modifications, I can send emails by manually entering a value at position C7. That is, according to script, if the value is greater than 100, it will send a email to me. That only happens if I type the number manually into the cell.
The problem is, if the value is generated by a formula, then it doesn't work. (Say cell C7 is a formula=C4*C5 where the product value is >100)
After some trial-and-error, I think it is the code in the edit detection part causing the problem.
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
Since cell C7 is a formula, the formula itself doesn't change, what is changing is the values from formula calculations. So it may not think I have edited the cell.
How should I modify the script, so that the script also send email when value of C7 produced by a formula is greater than 100?
For reference, here is the code that I am using.
function checkValue(e)
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
{
if(valueToCheck >100)
{
MailApp.sendEmail("h********#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
}
}
}
onEdit(e) Trigger(Both simple and Installable) will not trigger unless a human explicitly edits the file. In your case, Your seem to be getting value from an external source (specifically, Google finance data).
Script executions and API requests do not cause triggers to run. For example, calling FormResponse.submit() to submit a new form response does not cause the form's submit trigger to run.
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause the spreadsheet's onEdit trigger to run.
Also, for Google finance data,
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you will see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
Notes:
Having said that,
In cases where the change is made by a formula(like =IF(),=VLOOKUP()) other than auto-change formulas(like =GOOGLEFINANCE,=IMPORTRANGE,=IMPORTXML, etc), Usually a human would need to edit some other cell- In which case, You will be able to capture that event(Which caused a human edit) and make changes to the formula cell instead.
In cases where the change is done from sheets api, a installed onChange trigger may be able to capture the event.
To avoid repeated notifications due to edits in other places, send an email only when the value you are tracking is different from the previous one. Store the previous value in script properties.
function checkValue(e) {
var sp = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var oldValue = sp.getProperty("C7") || 0;
if (valueToCheck > 100 && valueToCheck != oldValue) {
MailApp.sendEmail("***v#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
sp.setProperty("C7", valueToCheck);
}
}
Here the email is sent only when valueToCheck differs from the stored value. If this happens, the stored value is updated by setProperty.
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().