Determine current user in Apps Script - google-apps-script

I'm trying to identify current user's name to make notes of who edited what like this:
r.setComment("Edit at " + (new Date()) + " by " + Session.getActiveUser().getEmail());
but it won't work - user's name is an empty string.
Where did I go wrong?

GOOD NEWS: It's possible with this workaround!
I'm using some protection functionality that reveals the user and owner of the document and I'm storing it in the properties for better performance. Have fun with it!
function onEdit(e) {
SpreadsheetApp.getUi().alert("User Email is " + getUserEmail());
}
function getUserEmail() {
var userEmail = PropertiesService.getUserProperties().getProperty("userEmail");
if(!userEmail) {
var protection = SpreadsheetApp.getActive().getRange("A1").protect();
// tric: the owner and user can not be removed
protection.removeEditors(protection.getEditors());
var editors = protection.getEditors();
if(editors.length === 2) {
var owner = SpreadsheetApp.getActive().getOwner();
editors.splice(editors.indexOf(owner),1); // remove owner, take the user
}
userEmail = editors[0];
protection.remove();
// saving for better performance next run
PropertiesService.getUserProperties().setProperty("userEmail",userEmail);
}
return userEmail;
}

I suppose you have this piece of code set to execute inside an onEdit function (or an on edit trigger).
If you are on a consumer account, Session.getActiveUser().getEmail() will return blank. It will return the email address only when both the author of the script and the user are on the same Google Apps domain.

I had trouble with Wim den Herder's solution when I used scripts running from triggers. Any non script owner was unable to edit a protected cell. It worked fine if the script was run from a button. However I needed scripts to run periodically so this was my solution:
When a user uses the sheet the first time he/she should click a button and run this:
function identifyUser(){
var input = Browser.inputBox('Enter User Id which will be used to save user to events (run once)');
PropertiesService.getUserProperties().setProperty("ID", input);
}
This saves the user's input to a user property. It can be read back later at any time with this code:
var user = PropertiesService.getUserProperties().getProperty("ID");

In this code you can use a cell for input. Authorising scripts are not required.
function onEdit(e){
checkUsername(e);
}
function checkUsername(e){
var sheet = e.source.getActiveSheet();
var sheetToCheck = 'Your profile';
var sheetName = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetToCheck);
var CellInputUsername = 'B4';
var ActiveCell = SpreadsheetApp.getActive().getActiveRange().getA1Notation();
if (sheet.getName() !== sheetToCheck || ActiveCell !== CellInputUsername){return;}
var cellInput = sheetName.getRange(CellInputUsername).getValue();
PropertiesService.getUserProperties().setProperty("Name", cellInput);
// Make cell empty again for new user
sheetName.getRange(CellInputUsername).setValue("");
var Username = PropertiesService.getUserProperties().getProperty("Name");
SpreadsheetApp.getUi().alert("Hello " + Username);
}

Related

Apps Script - onEdit combine timestamp and username/email address not working

I'm attempting to pull the ActiveUser and Email Address of the person making the change to the Google Sheet and thought it would be possible using the following, but all I can get is the timestamp. If I change "var obj2 = (Session.getActiveUser().getEmail()); to literally anything else, it pulls the new data so I feel like the rest of my script is fine but can't figure out why it never pulls a username or address...
Any thoughts as to what I'm doing wrong? I heard that possibly google does not allow this information to be pulled via onEdit anymore? If that's the case is there another way?
DOESN'T WORK
function onEdit(e) {
var r = e.range;
var ss = r.getSheet();
// Prepare an object for searching sheet name.
var obj = {'SHEET1': "D", 'SHEET2': "G"};
var obj2 = (Session.getActiveUser().getEmail());
// Using the object, check the sheet and put or clear the range.
if (r.getColumn() == 1 && obj[ss.getSheetName()]) {
var celladdress = obj[ss.getName()] + r.getRowIndex();
if (r.isChecked()) {
ss.getRange(celladdress).setValue(new Date()).setNumberFormat(("MM/DD/YYYY hh:mm:ss") + " " + obj2);
} else {
ss.getRange(celladdress).clearContent();
}
}
}
DOES WORK (spits out Timestamp with a space and the word TEST at the end. Test is where I'd expect the username/email in the first example that doesn't currently work)
function onEdit(e) {
var r = e.range;
var ss = r.getSheet();
// Prepare an object for searching sheet name.
var obj = {'SHEET1': "D", 'SHEET2': "G"};
var obj2 = ("TEST");
// Using the object, check the sheet and put or clear the range.
if (r.getColumn() == 1 && obj[ss.getSheetName()]) {
var celladdress = obj[ss.getName()] + r.getRowIndex();
if (r.isChecked()) {
ss.getRange(celladdress).setValue(new Date()).setNumberFormat(("MM/DD/YYYY hh:mm:ss") + " " + obj2);
} else {
ss.getRange(celladdress).clearContent();
}
}
}
It is limited for consumer accounts
From:
https://developers.google.com/apps-script/guides/triggers/events under User
A User object, representing the active user, if available (depending on a complex set of security restrictions).
From:
https://developers.google.com/apps-script/reference/base/user#getemail
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.
If you are in a Workspace Domain then what you need to do is to make it an installable trigger. Then anyone in your domain who edits it, will appear in the logs if you call:
Logger.log(e.user.getEmail())
But for consumer (gmail) accounts this is far more limited, usually returning just a blank string. This is for security as much as anything, because without a policy like this, someone might use Sheets as a way to mine e-mail addresses.

How do I send an automated email to a specific person, depending on task status, using an aux sheet to store emails?

Gory title but I couldn't find a way of being clearer.
I have no experience with coding and I was wondering if doing something like what I'm about to explain would be possible.
This is my example sheet:
What I'm looking to do is to have automated emails sent out to the person assigned to the task if the task status is set to urgent, while referencing people by names and having an auxiliary sheet with all the names and corresponding emails.
I've browsed around and found some similar questions which I unfortunately had no success in adapting. The one thing I got is that I need to setup an onEdit trigger, which I've done, but I'm completely clueless from here on out.
Can someone point me in the right direction? I don't have a clue where to start.
Looking forward to hearing your advice.
Thanks and stay safe in these crazy times!
It was a funny exercise. I tried to make the script as clean and reusable as possible for others to be able to adapt it to their needs.
Usage
Open spreadsheet you want to add script to.
Open Script Editor: Tools / Script editor.
Add the code. It can be configured by adjusting variables in the top:
var trackerSheetName = 'Tracker 1'
var trackerSheetStatusColumnIndex = 2
var trackerSheetNameColumnIndex = 4
var triggeringStatusValue = 'Urgent'
var peopleSheetName = 'AUX'
var peopleSheetNameColumnIndex = 1
var peopleSheetEmailColumnIndex = 2
var emailSubject = 'We need your attention'
var emailBody = 'It is urgent'
function checkStatusUpdate(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
var activeSheet = spreadsheet.getActiveSheet()
// skip if different sheet edited
if (activeSheet.getName() !== trackerSheetName) {
return
}
var editedRange = e.range
// skip if not a single cell edit
if (editedRange.columnStart !== editedRange.columnEnd || editedRange.rowStart !== editedRange.rowEnd) {
return
}
// skip if edited cell is not from Status column
if (editedRange.columnStart !== trackerSheetStatusColumnIndex) {
return
}
// skip if Status changed to something other than we're looking for
if (e.value !== triggeringStatusValue) {
return
}
var assigneeName = activeSheet.getRange(editedRange.rowStart, trackerSheetNameColumnIndex, 1, 1).getValue()
var peopleSheet = spreadsheet.getSheetByName(peopleSheetName)
var people = peopleSheet.getRange(2, 1, peopleSheet.getMaxRows(), peopleSheet.getMaxColumns()).getValues()
// filter out empty rows
people.filter(function (person) {
return person[peopleSheetNameColumnIndex - 1] && person[peopleSheetEmailColumnIndex - 1]
}).forEach(function (person) {
if (person[peopleSheetNameColumnIndex - 1] === assigneeName) {
var email = person[peopleSheetEmailColumnIndex - 1]
MailApp.sendEmail(email, emailSubject, emailBody)
}
})
}
Save the code in editor.
Open Installable Triggers page: Edit / Current project's triggers.
Create a new trigger. Set Event Type to On edit. Keep other options default.
Save the Trigger and confirm granting the script permissions to access spreadsheets and send email on your behalf.
Go back to your spreadsheet and try changing status in Tracker 1 tab for any of the rows. Corresponding recipient should receive an email shortly.
This should get you started:
You will need to create an installable trigger for onMyEdit function. The dialog will help you to design you email by giving you an html format to display it. When you're ready just comment out the dialog and remove the // from in front of the GmailApp.sendEdmail() line.
function onMyEdit(e) {
//e.source.toast('Entry');
const sh=e.range.getSheet();
if(sh.getName()=="Tracker") {
if(e.range.columnStart==2 && e.value=='Urgent') {
//e.source.toast('flag1');
const title=e.range.offset(0,-1).getValue();
const desc=e.range.offset(0,1).getValue();
const comm=e.range.offset(0,3).getValue();
if(title && desc) {
var html=Utilities.formatString('<br />Task Title:%s<br />Desc:%s<br />Comments:%s',title,desc,comm?comm:"No Additional Comments");
//GmailApp.sendEmail(e.range.offset(0,2).getValue(), "Urgent Message from Tracker", '',{htmlBody:html});
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(html).setWidth(600), 'Tracker Message');
e.source.toast('Email Sent');
}else{
e.source.toast('Missing Inputs');
}
}
}
}
GmailApp.sendEmail()

How can I trigger a function when switching sheets within a spreadsheet?

How can I trigger a function when switching sheets? The main goal is to track how people is using a spreadsheet. I found the following option that does exactly this, but it is only tracking the case where I switch between sheet, but not when other people do it:
var PROP_NAME = "lastSheetIdx";
var userProperties = PropertiesService.getUserProperties();
function timedEventHandler() {
var currentSheetIdx = SpreadsheetApp.getActiveSheet().getIndex()
var previousSheetIdx = parseInt(userProperties.getProperty(PROP_NAME));
if (currentSheetIdx !== previousSheetIdx) {
didSwitchSheets(previousSheetIdx, currentSheetIdx);
userProperties.setProperty(PROP_NAME, currentSheetIdx);
}
}
function didSwitchSheets(from, to) {
var sheet =SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Engagement");
Logger.log("Switched from " + from + " to " + to);
sheet.appendRow([new Date(), to, Session.getActiveUser().getEmail()]);
}
Any advice how can I change this script, so it will track anyone (getting their email) switching between sheets?
For context, we use GSuite, so it should be ok for us to pull the email addresses being the same domain.

GAS: Script authorization via custom UI menu fails with "You do not have access to perform that action."

I have a spreadsheet with a bound script that tries to identify users currently making edits to the sheet, and appends edit info into specified columns of the sheet.
It seems that users always have to undergo the initial script authorization (that Google popup that asks for permissions) by opening the Script Editor and running a function manually via the "play" button. Invoking the function via a custom UI menu doesn't work and throws an error: "You do not have access to perform that action. Please ask the owner of this item to grant access to you."
Is there any way to invoke authorization in a more user-friendly way that doesn't require the users opening the Scripts Editor?
I thought making the menu was the point of that (manual execution vs. onEdit() or onOpened() triggered execution), but as it stands, this workaround is useless - I don't need the menu if the users have to open the Editor to achieve the same thing.
Full code:
function onEdit(e) {
var sheet = e.source.getActiveSheet();
var labels = e.source.getRangeByName("labels");
if (labels == null) {
Logger.log("Error: couldn't find named range 'labels'!");
return;
}
var found = false;
for(var i = 1; i <= labels.getNumColumns(); i++){
if (labels.getCell(1,i).getValue() == "last edit") {
found = true;
break;
}
}
if (!found) {
Logger.log("Error: couldn't find column 'last edit'!");
return;
}
var minrow = labels.getRowIndex();
var range = sheet.getActiveRange();
var row = range.getRowIndex();
// don't track edits above the labels
if (row <= minrow) { return; }
var range_depth = range.getNumRows();
var rangeA1 = range.getA1Notation();
var timestamp = Utilities.formatDate(new Date(), "GMT+2", "YYYY-MM-dd HH:mm");
// the elegant way - requires the sheet owner and users to be on the same domain:
// var user = Session.getActiveUser().getEmail();
// the workaround - requires the user to identify themselves manually:
var user = PropertiesService.getUserProperties().getProperty("ID");
if (user == null) {
Browser.msgBox('Current user unidentified. Run the identification script via the "Edit tracking" menu (next to "Help").');
user = 'unknown';
}
for (var j = 0; j < range_depth; j++) {
sheet.getRange(row+j, i).setValue(timestamp);
sheet.getRange(row+j, i+1).setValue(user);
sheet.getRange(row+j, i+2).setValue(rangeA1);
}
SpreadsheetApp.flush();
}
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Edit tracking')
.addItem('Identify current user', 'showIdentify')
.addItem('Forget current user', 'showForget')
.addToUi();
}
function showIdentify() {
var ui = SpreadsheetApp.getUi();
var result = ui.prompt(
'Edit tracking',
'Please enter your name:',
ui.ButtonSet.OK_CANCEL);
var button = result.getSelectedButton();
var text = result.getResponseText();
if (button == ui.Button.OK) {
PropertiesService.getUserProperties().setProperty("ID", text);
} else {
ui.alert('Look, you\'re not dodging this, I\'ll nag you again on the next edit.');
}
}
function showForget() {
var ui = SpreadsheetApp.getUi();
PropertiesService.getUserProperties().deleteProperty("ID");
ui.alert('You\'re anonymous now.');
}
It seems to me like a bug in google sheets that the script is not shared along with the sheet.
But to make it more user friendly you can
A) publicly share your sheet (Anyone on the internet can find and edit) or
B) (using "anyone with the link can edit" sharing) In the script editor get the share link File->Share... and have the user open that link.
Then it will work.

Get the user who clicked on the button in the spreadsheet

I have added the button into spreadsheet and assigned the script to it. Is there a way to determine the user's email of person that clicked it? Script edits the data so probably onEdit trigger should work, however function with Session.getActiveUser().getEmail() set by this trigger doesn't recognize the user.
Thank you
It's possible with normal gmail accounts, with this workaround!
I'm using some protection functionality that reveals the user and owner of the document and I'm storing it in the properties for better performance. Have fun with it!
function onEdit(e) {
SpreadsheetApp.getUi().alert("User Email is " + getUserEmail());
}
function getUserEmail() {
var userEmail = PropertiesService.getUserProperties().getProperty("userEmail");
if(!userEmail) {
var protection = SpreadsheetApp.getActive().getRange("A1").protect();
// tric: the owner and user can not be removed
protection.removeEditors(protection.getEditors());
var editors = protection.getEditors();
if(editors.length === 2) {
var owner = SpreadsheetApp.getActive().getOwner();
editors.splice(editors.indexOf(owner),1); // remove owner, take the user
}
userEmail = editors[0];
protection.remove();
// saving for better performance next run
PropertiesService.getUserProperties().setProperty("userEmail",userEmail);
}
return userEmail;
}