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

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.

Related

Google Apps Script for Yahoo Finance Returns Empty Cell

A previously working solution that was resolved here by #tanaike suddenly returns an empty cell upon execution. I don't get an error message and in the google apps scripts edit page I get "Notice Execution completed".
It looks like it's working in the background but having trouble returning a value to the cell, my guess would be something wrong with the last line that may resolve it?
function pressReleases(code) {
var url = 'https://finance.yahoo.com/quote/' + code + '/press-releases'
var html = UrlFetchApp.fetch(url).getContentText().match(/root.App.main = ([\s\S\w]+?);\n/);
if (!html || html.length == 1) return;
var obj = JSON.parse(html[1].trim());
// --- I modified the below script.
const { _cs, _cr } = obj;
if (!_cs || !_cr) return;
const key = CryptoJS.algo.PBKDF2.create({ keySize: 8 }).compute(_cs, JSON.parse(_cr)).toString();
const obj2 = JSON.parse(CryptoJS.enc.Utf8.stringify(CryptoJS.AES.decrypt(obj.context.dispatcher.stores, key)));
var res = obj2.StreamStore.streams["YFINANCE:" + code + ".mega"].data.stream_items[0].title;
// ---
return res || "No value";
}
The CryptoJS code saved as a script in google apps script is here
When I tested this script, at if (!_cs || !_cr) return;, I confirmed that the values of _cs and _cr are undefined. From this result, I understood that recently, the specification of the key for decrypting the data has been changed at the server side. When I saw this thread, I confirmed the same situation. In the thread, I noticed that, in the current stage, the key can be simply retrieved from the HTML data. So, as with the current script, how about the following modification?
Usage:
1. Get crypto-js.
Please access https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js. And, copy and paste the script to the script editor of Google Apps Script, and save the script.
2. Modify script.
function pressReleases(code) {
var url = 'https://finance.yahoo.com/quote/' + code + '/press-releases'
var html = UrlFetchApp.fetch(url).getContentText().match(/root.App.main = ([\s\S\w]+?);\n/);
if (!html || html.length == 1) return;
var obj = JSON.parse(html[1].trim());
var key = Object.entries(obj).find(([k]) => !["context", "plugins"].includes(k))[1];
if (!key) return;
const obj2 = JSON.parse(CryptoJS.enc.Utf8.stringify(CryptoJS.AES.decrypt(obj.context.dispatcher.stores, key)));
var res = obj2.StreamStore.streams["YFINANCE:" + code + ".mega"].data.stream_items[0].title;
// console.log(res); // Check the value in the log.
return res || "No value";
}
When this script is run with code = "PGEN", the value of Precigen Provides Pipeline and Corporate Updates at the 41st Annual J.P. Morgan Healthcare Conference is obtained.
Note:
If you want to load crypto-js directly, you can also use the following script. But, in this case, the process cost becomes higher than that of the above flow. Please be careful about this.
const cdnjs = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js";
eval(UrlFetchApp.fetch(cdnjs).getContentText());
I can confirm that this method can be used for the current situation (January 14, 2023). But, when the specification in the data and HTML is changed in the future update on the server side, this script might not be able to be used. Please be careful about this.
Reference:
crypto-js

Google sheet sends an email when a new form is submitted if a question has a particular answer

I hope you can help, I have tried to find a solution but I am unable to find one that resolves my issue.
I have a form that collects information if a student request a change to their course, we are a multi school site and depending on the site, a different member of staff is required to approve the change.
Part of the information collect is the site code OAAd for example, and this information is held in column F.
If a submission is received the 'On form submit' trigger, triggers the script. Currently it doesn't seem to work, yet it triggers without error. If I change the trigger 'On edit' and edit a cell in column F it works fine.
eventually I will use ifElse to add in the additional options for each site, but if I could get it to work for one site that would be great.
function sendEmailapproval(e) {
if(e.range.getColumn()==6 && e.value=='OAAd'){
var emailRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Email Addresses").getRange("B3");
var recipent = emailRange.getValue();
var html = HtmlService.createTemplateFromFile("Email1.html");
var htmlText = html.evaluate().getContent();
var subject = "New Change Log Request Post 16";
var body = "";
var options = { htmlBody: htmlText}
MailApp.sendEmail(recipent, subject, body, options)
}
}
Background
There are 2 options to add an onFormSubmit trigger:
Using the form settings / script.
Using the attached spreadsheet script.
Both methods gives an event object to communicate with new submmited responese, but threre are differaces between those objet so you need to dicide in advance which method to choose.
You can read more about this topic here.
For your propose, would preder to use the spreadsheet script, since it easier to extract specific values from it.
Solution
First, Here is your correct sendEmailapproval function:
function sendEmailapproval(e) {
const namedValues = e.namedValues
const question = 'Question 3'; // copy-paste from the form
switch (namedValues[question][0])
{
case 'OAAd':
const emailRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Email Addresses").getRange("B3");
const recipent = emailRange.getValue();
const html = HtmlService.createTemplateFromFile("Email1.html");
const htmlText = html.evaluate().getContent();
const subject = "New Change Log Request Post 16";
const body = "";
const options = { htmlBody: htmlText}
MailApp.sendEmail(recipent, subject, body, options)
break;
// case 'SOMETHING_ELSE':
// break;
default:
}
}
Note that e.namedValues is an object, where each key mapped to an array.
So namedValues[question][0] refers to the string value of namedValues[question].
Second, add a trigger to the spreadsheet that attached to the form.
function createTrigger() {
const sheet = SpreadsheetApp.openById("YOUR_SPREADSHEET_ID").getSheetByName("THE_SHEET_NAME");
ScriptApp.newTrigger("sendEmailapproval")
.forSpreadsheet(sheet)
.onFormSubmit()
.create();
}

Google Sheets Scraping Options Chain from Yahoo Finance, Incomplete Results [duplicate]

This question already has answers here:
Scraping data to Google Sheets from a website that uses JavaScript
(2 answers)
Closed last month.
I'm attempting to scrape options pricing data from Yahoo Finance in Google Sheets. Although I'm able to pull the options chain just fine, i.e.
=IMPORTHTML("https://finance.yahoo.com/quote/TCOM/options?date=1610668800","table",2)
I find that it's returning results that don't completely match what's actually shown on Yahoo Finance. Specifically, the scraped results are incomplete - they're missing some strikes. i.e. the first 5 rows of the chart may match, but then it will start returning only every other strike (aka skipping every other strike).
Why would IMPORTHTML be returning "abbreviated" results, which don't match what's actually shown on the page? And more importantly, is there some way to scrape complete data (i.e. that doesn't skip some portion of the available strikes)?
In Yahoo finance, all data are available in a big json called root.App.main. So to get the complete set of data, proceed as following
var source = UrlFetchApp.fetch(url).getContentText()
var jsonString = source.match(/(?<=root.App.main = ).*(?=}}}})/g) + '}}}}'
var data = JSON.parse(jsonString)
You can then choose to fetch the informations you need. Take a copy of this example https://docs.google.com/spreadsheets/d/1sTA71PhpxI_QdGKXVAtb0Rc3cmvPLgzvXKXXTmiec7k/copy
edit
if you want to get a full list of available data, you can retrieve it by this simple script
// mike.steelson
let result = [];
function getAllDataJSON(url = 'https://finance.yahoo.com/quote/TCOM/options?date=1610668800') {
var source = UrlFetchApp.fetch(url).getContentText()
var jsonString = source.match(/(?<=root.App.main = ).*(?=}}}})/g) + '}}}}'
var data = JSON.parse(jsonString)
getAllData(eval(data),'data')
var sh = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
sh.getRange(1, 1, result.length, result[0].length).setValues(result);
}
function getAllData(obj,id) {
const regex = new RegExp('[^0-9]+');
for (let p in obj) {
var newid = (regex.test(p)) ? id + '["' + p + '"]' : id + '[' + p + ']';
if (obj[p]!=null){
if (typeof obj[p] != 'object' && typeof obj[p] != 'function'){
result.push([newid, obj[p]]);
}
if (typeof obj[p] == 'object') {
getAllData(obj[p], newid );
}
}
}
}
Here's a simpler way to get the last market price of a given option. Add this function to you Google Sheets Script Editor.
function OPTION(ticker) {
var ticker = ticker+"";
var URL = "finance.yahoo.com/quote/"+ticker;
var html = UrlFetchApp.fetch(URL).getContentText();
var count = (html.match(/regularMarketPrice/g) || []).length;
var query = "regularMarketPrice";
var loc = 0;
var n = parseInt(count)-2;
for(i = 0; i<n; i++) {
loc = html.indexOf(query,loc+1);
}
var value = html.substring(loc+query.length+9, html.indexOf(",", loc+query.length+9));
return value*100;
}
In your google sheets input the Yahoo Finance option ticker like below
=OPTION("AAPL210430C00060000")
I believe your goal as follows.
You want to retrieve the complete table from the URL of https://finance.yahoo.com/quote/TCOM/options?date=1610668800, and want to put it to the Spreadsheet.
Issue and workaround:
I could replicate your issue. When I saw the HTML data, unfortunately, I couldn't find the difference of HTML between the showing rows and the not showing rows. And also, I could confirm that the complete table is included in the HTML data. By the way, when I tested it using =IMPORTXML(A1,"//section[2]//tr"), the same result of IMPORTHTML occurs. So I thought that in this case, IMPORTHTML and IMPORTXML might not be able to retrieve the complete table.
So, in this answer, as a workaround, I would like to propose to put the complete table parsed using Sheets API. In this case, Google Apps Script is used. By this, I could confirm that the complete table can be retrieved by parsing the HTML table with Sheet API.
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet, and please enable Sheets API at Advanced Google services. And, please run the function of myFunction at the script editor. By this, the retrieved table is put to the sheet of sheetName.
function myFunction() {
// Please set the following variables.
const url ="https://finance.yahoo.com/quote/TCOM/options?date=1610668800";
const sheetName = "Sheet1"; // Please set the destination sheet name.
const sessionNumber = 2; // Please set the number of session. In this case, the table of 2nd session is retrieved.
const html = UrlFetchApp.fetch(url).getContentText();
const section = [...html.matchAll(/<section[\s\S\w]+?<\/section>/g)];
if (section.length >= sessionNumber) {
if (section[sessionNumber].length == 1) {
const table = section[sessionNumber][0].match(/<table[\s\S\w]+?<\/table>/);
if (table) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const body = {requests: [{pasteData: {html: true, data: table[0], coordinate: {sheetId: ss.getSheetByName(sheetName).getSheetId()}}}]};
Sheets.Spreadsheets.batchUpdate(body, ss.getId());
}
} else {
throw new Error("No table.");
}
} else {
throw new Error("No table.");
}
}
const sessionNumber = 2; means that 2 of =IMPORTHTML("https://finance.yahoo.com/quote/TCOM/options?date=1610668800","table",2).
References:
Method: spreadsheets.batchUpdate
PasteDataRequest

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);
}