How can I trigger a function when switching sheets within a spreadsheet? - google-apps-script

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.

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 can I remove or programmatically disable/block an attached script on a copied Google Doc?

I am looking to simulate the behavior of Google Classroom's assignment feature that allows you to make a copy of a document for a list of students. This copy retains ownership by the creator and adds the student as an editor.
I have succeed in making this work with a script attached to a template doc. The teachers can paste a list of email addresses into a custom pull-out, and on submission a loop creates each doc with the permissions.
Attempts:
I can use copy() functionality to copy the doc, but the attached script goes along for the ride and is then accessible by the students. This is not a major security risk, but has the potential to be abused.
I can have moderate success by using the regularly mentioned method of looping through all elements and appending them to the new doc, but so far I have not been able to make everything work in this way. Some images, tables, and other elements that teachers might create do not format properly in the new doc.
Hopeful solutions:
Is there a way to remove the script from the copied doc? or
Is there a way to use permissions to only allow the script to be run by faculty? (We do have an Org Unit for faculty, but my tests with the AdminDirectory module leave me concerned about permissions once all faculty is using the tool.) or
Knowing that our student email addresses are formatted differently from our faculty email addresses, could I programmatically block the script based on email address parsing of the current user?
I've gone in circles and keep ending up at the posts explaining how to copy elements one at a time into a new doc. This does not appear to be sufficient due to formatting so I'm hoping one of the other solutions involving keeping the copy() function is possible.
Sidebar Code sidebar.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
</script>
<style>
textarea{
font-size: .9em;
width: 90%;
height: 300px;
}
</style>
</head>
<body>
<textarea id="addressList">
Paste class email list here with spaces between addresses.
</textarea>
<p>
<input id="submit" type="button" value="Distribute to Students" onclick="distributor();" />
<input id="reset" type="button" value="Reset" onclick="reset();" />
</p>
<script>
function reset(){
document.getElementById('addressList').value = "Paste class email list here with spaces between addresses.";
document.getElementById("submit").disabled = false;
}
var mainOut = "";
function cleanUp(output){
mainOut += output;
document.getElementById('addressList').value = mainOut;
}
function distributor(){
document.getElementById("submit").disabled = true;
var nameList = document.getElementById('addressList').value; // get email list
var list = nameList.split("\n"); // break it up
// loop through names
for(let i = 0; i < list.length; i++){
// create a doc for each student and return success to the UI
google.script.run.withSuccessHandler(cleanUp).distro(list[i],i+1,list.length);
}
}
</script>
</body>
</html>
Current Script Code.gs
// student email addresses end in a 2 digit class year, faculty does not
var event;
var emailParts = Session.getEffectiveUser().getEmail().split('#');
var username = emailParts[0];
var classCheck = username.substring(username.length-2);
var validUser = false;
// using school email address style, determine if student or teacher to hide the UI changes
if(isNaN(classCheck)){
validUser = true;
}
// Custom Menu
function onOpen(e){
if(validUser){
event = e;
var ui = DocumentApp.getUi();
var faMenu = ui.createMenu('FA')
.addItem('Open Distribution Tool', 'openTool').addToUi();
}
}
/**
* Creates Custom Sidebar for emailing teams from spreadsheet
*/
function openTool() {
if(validUser){
var html = HtmlService.createHtmlOutputFromFile('sidebar');
html.setTitle('Share with Students');
html.setWidth(400);
html.setContent(html.getContent());
DocumentApp.getUi().showSidebar(html);
}
}
// create copy, set permissions
function distro(studentEmail,count,total){
var output = "";
if(validUser){
var teacherEmail = 'xxx#xxx.org'; // this will be replaced by email of logged in user
var thisDoc = DocumentApp.getActiveDocument();
let studentName = studentEmail.split('#')[0];
if(studentEmail != "" && studentEmail != null){
let filename = thisDoc.getName() + "- " + studentName;
let newDoc = DriveApp.getFileById(thisDoc.getId()).makeCopy(filename);
newDoc.setOwner(studentEmail);
newDoc.addEditor(teacherEmail);
output += "Created doc " + count + "/" + total + ": " + filename + "\n";
}
}
return output;
}
Issue:
I don't think you can programmatically remove the bound script from a copied document.
In theory, this is possible if you use Apps Script API, by calling projects.updateContent and set and empty content for your Files.
Nevertheless, this requires knowing the scriptId, and you cannot programmatically retrieve the scriptId of a bound script which is not the current one (for the current one, Session.getScriptId() can be used). See this answer, for example, and this related feature request:
Retrieving Project ID of Container-Bound script
Workaround - use libraries:
As a workaround, I'd suggest putting at least some of the script code in a different, standalone script, and make your template call this library. This way, the library source code would not be available to the script bound to the copied file, which could only run the different functions defined in the library.
For example, you could move onOpen and distro to another script:
Library Code.gs:
// student email addresses end in a 2 digit class year, faculty does not
var event;
var emailParts = Session.getEffectiveUser().getEmail().split('#');
var username = emailParts[0];
var classCheck = username.substring(username.length-2);
var validUser = false;
// using school email address style, determine if student or teacher to hide the UI changes
if(isNaN(classCheck)){
validUser = true;
}
// Custom Menu
function onOpen(e){
if(validUser){
event = e;
var ui = DocumentApp.getUi();
var faMenu = ui.createMenu('FA')
.addItem('Open Distribution Tool', 'openTool').addToUi();
}
}
// create copy, set permissions
function distro(studentEmail,count,total){
var output = "";
if(validUser){
var teacherEmail = 'xxx#xxx.org'; // this will be replaced by email of logged in user
var thisDoc = DocumentApp.getActiveDocument();
let studentName = studentEmail.split('#')[0];
if(studentEmail != "" && studentEmail != null){
let filename = thisDoc.getName() + "- " + studentName;
let newDoc = DriveApp.getFileById(thisDoc.getId()).makeCopy(filename);
newDoc.setOwner(studentEmail);
newDoc.addEditor(teacherEmail);
output += "Created doc " + count + "/" + total + ": " + filename + "\n";
}
}
return output;
}
Then, share this script as a library, and use it in your template script, which could be like this (where LIBRARY is the identifier for your previously shared library):
Template Code.gs:
// student email addresses end in a 2 digit class year, faculty does not
var event;
var emailParts = Session.getEffectiveUser().getEmail().split('#');
var username = emailParts[0];
var classCheck = username.substring(username.length-2);
var validUser = false;
// using school email address style, determine if student or teacher to hide the UI changes
if(isNaN(classCheck)){
validUser = true;
}
// Custom Menu
function onOpen(e){
LIBRARY.onOpen(e);
}
/**
* Creates Custom Sidebar for emailing teams from spreadsheet
*/
function openTool() {
if(validUser){
var html = HtmlService.createHtmlOutputFromFile('sidebar');
html.setTitle('Share with Students');
html.setWidth(400);
html.setContent(html.getContent());
DocumentApp.getUi().showSidebar(html);
}
}
// create copy, set permissions
function distro(studentEmail,count,total){
LIBRARY.distro(studentEmail, count, total);
}
In this example, sidebar.html would also be contained in your template script.
Note:
This is a basic example just to show how this could be done, and could probably be improved. For example, it should be possible to also move openTool and the .html file to the library code, even though calling distro via google.script.run could become tricky: see Call Library function from html with google.script.run.
Reference:
Apps Script: Libraries

This script does not populate sheet after parsing retrieved data

I hope this is well explained. First of all, sorry because my coding background is zero and I am just trying to "fix" a previously written script.
Problem The script does not populate sheet after parsing retrieved data if the function is triggered by timer and the sheet is not open in my browser .
The script works OK if run it manually while sheet is open.
Problem details:
When I open the sheet the cells are stuck showing "Loading" and after a short time, data is written.
Expected behavior is to get the data written no matter if I don't open the sheet.
Additional info: This is how I manually run the function
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [
{name: "Manual Push Report", functionName: "runTool"}
];
sheet.addMenu("PageSpeed Menu", entries);
}
Additional info: I set the triggers with Google Apps Script GUI See the trigger
Before posting the script code, you can see how the cells look in the sheet:
Script code
function runTool() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Results");
var rows = activeSheet.getLastRow();
for(var i=3; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 2).getValue();
var stuff = "=runCheck"
if(workingCell != ""){
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
}
}
}
// URL check //
function runCheck(Url) {
var key = "XXXX Google PageSpeed API Key";
var strategy = "desktop"
var serviceUrl = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=" + Url + "&key=" + key + "&strategy=" + strategy +"";
var array = [];
var response = UrlFetchApp.fetch(serviceUrl);
if (response.getResponseCode() == 200) {
var content = JSON.parse(response.getContentText());
if ((content != null) && (content["lighthouseResult"] != null)) {
if (content["captchaResult"]) {
var score = content["lighthouseResult"]["categories"]["performance"]["score"];
} else {
var score = "An error occured";
}
}
array.push([score,"complete"]);
Utilities.sleep(1000);
return array;
}
}
You can try the code using the sheet below with a valid Pagespeed API key.
You only need to add a Trigger and wait for it's execution while the sheet is not open in your browser
https://docs.google.com/spreadsheets/d/1ED2u3bKpS0vaJdlCwsLOrZTp5U0_T8nZkmFHVluNvKY/copy
I suggest you to change your algorithm. Instead of using a custom function to call UrlFetchApp, do that call in the function called by a time-driven trigger.
You could keep your runCheck as is, just replace
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
by
activeSheet.getRange(i, 3, 1, 2).setValues(runCheck(url));
NOTE
Custom functions are calculated when the spreadsheet is opened and when its arguments changes while the spreadsheet is open.
Related
Cache custom function result between spreadsheet opens

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()

Determine current user in 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);
}