I have been working with Google Apps Script lately and found a way to send HTML email containing a table in Google Sheets https://spreadsheet.dev/send-html-email-from-google-sheets#:~:text=Search%20for%20Content%2DType%3A%20text,the%20document%20except%20this%20HTML. But I am not able to configure how I should be doing this with multiple tables (4 in my case) in a single mail only?
function sendEmail() {
var stockData = getData();
var body = getEmailText(stockData);
MailApp.sendEmail({
to: "youremail#example.com",
subject: "Stock update",
body: body
});
}
function getEmailText(stockData) {
var text = "";
stockData.forEach(function(stock) {
text = text + stock.name + "\n" + stock.ticker + "\n" + stock.price + "\n-----------------------\n\n";
});
return text;
}
/**
* #OnlyCurrentDoc
*/
function getData() {
var values = SpreadsheetApp.getActive().getSheetByName("Data").getRange("Stocks").getValues();
values.shift(); //remove headers
var stocks = [];
values.forEach(function(value) {
var stock = {};
stock.name = value[0];
stock.ticker = value[1];
stock.price = value[2];
stocks.push(stock);
})
//Logger.log(JSON.stringify(stocks));
return stocks;
}
The above code works perfectly well for a single table. What modifications can I make to have multiple tables? Any help will be much appreciated.
I believe your goal is as follows.
You want to retrieve the values with the font styles and background color of the cells from the named ranges and want to create the HTML table and send it as an email.
In your one table, only the header row has the specific background color. And the background color of other rows is #ffffff. This is from your sample images. image1, image2.
Modification points:
In your script, the values from the named range are not converted to the HTML. And, your script used one named range. I thought that this might be the reason for your issue.
In order to achieve your goal, how about the following flow?
Retrieve the values from all named ranges including the font styles and the background colors.
Crete HTML tables from the retrieved values.
Send an email using the created HTML tables.
When this flow is reflected in a sample script, it becomes as follows.
Sample script:
In this sample script, in order to covert from the richtext to HTML, "RichTextApp" of a Google Apps Script library is used. So before you use this script, please install the library.
function myFunction() {
const namedRanges = ["sampleNamedRange1", "sampleNamedRange2",,,]; // Please set your named ranges in this array.
const emailAddress = "youremail#example.com"; // Please set the email address.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const { htmls, backgounds } = namedRanges.reduce((o, r) => {
const range = ss.getRangeByName(r);
o.htmls.push(RichTextApp.RichTextToHTMLForSpreadsheet({ range }));
o.backgounds.push(range.getBackgrounds());
return o;
}, { htmls: [], backgounds: [] });
const tables = htmls.map((v, i) => `table ${i + 1}<br><table border="1" style="border-collapse: collapse"><tr style="background: ${backgounds[i][0][0]};">${v.map(r => "<td>" + r.join("</td><td>")).join("</tr><tr>")}</tr></table>`);
MailApp.sendEmail({ to: emailAddress, subject: "Stock update", htmlBody: tables.join("<br>") });
}
When this script is run, the values are retrieved from all named ranges by including the font styles and the background color of cells, and the retrieved values are converted to the HTML tables, and then, the HTML tables are sent as an email.
References:
RichTextApp
sendEmail(message) of Class MailApp
Edit:
I updated RichTextApp. By this, the HTML table can be directly converted from the range of Spreadsheet to a HTML table. Ref When this is used, the sample script is as follows. Before you use this script, please install the library.
Sample script:
function myFunction() {
const namedRanges = ["sampleNamedRange1", "sampleNamedRange2",,,]; // Please set your named ranges in this array.
const emailAddress = "youremail#example.com"; // Please set the email address.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const htmlTables = namedRanges.map(r => RichTextApp.RangeToHTMLTableForSpreadsheet({range: ss.getRangeByName(r)}));
const tables = htmlTables.map((t, i) => `table ${i + 1}<br>${t}`);
MailApp.sendEmail({ to: emailAddress, subject: "Stock update", htmlBody: tables.join("<br>") });
}
Related
I have a script that creates the client's folder, subfolders, and sheets in the drive whenever I add a new client to the sheet through HTML form. I am looking for a way to add the link in column K for that newly created folder, and also share the link to that folder on the client's email with edit access.
Here is the code that adds template folders in the drive. By the way am new to apps script. So am just learning and taking guidance from experts. Any help is warmly thanked.
function createClientAccountFolder(clientNamefolder, costingSheetId, ProposalDocId, renamesheet) {
// This is the root directory where all the client folders would be stored
const customerRoot = DriveApp.getFolderById('folder_id');
var ss = SpreadsheetApp.getActiveSpreadsheet();
var databaseSheet = ss.getSheetByName("Clients");
var lastAvailableRow = databaseSheet.getLastRow();
const clientName = databaseSheet.getRange(lastAvailableRow, 2).getValue();
if (clientNamefolder) {
// This creates the main folder with the customer name
const clients_sheet = ss.getRange("K2:K" + lastAvailableRow);
const mainFolder = customerRoot.createFolder(clientNamefolder);
var folder_url = mainFolder.getUrl();
// have tried a way to add the link in cell
clients_sheet.setFormula('=HYPERLINK("' + mainFolder.getUrl() + '")');
// Creating the sub folders
// Some are assigned to variables so that later children can be added
const action_tracker = mainFolder.createFolder("[SEO] - Action Tracker [" + clientNamefolder + "]")
const audit = mainFolder.createFolder("[SEO] - Audits [" + clientNamefolder + "]")
// Creating children
content.createFolder("[" + clientName + "] Content Approved")
content.createFolder("[" + clientName + "] Content Draft")
// Getting the documents
const action_tracker_sheet = DriveApp.getFileById('Sheet_id')
const analytics_audit = DriveApp.getFileById('Sheet_id')
// Moving the documents to the respective folders
const moved_action_tracker_sheet = action_tracker_sheet.moveTo(action_tracker)
const moved_analytics_audit = analytics_audit.moveTo(audit)
// renamin sheets
moved_action_tracker_sheet.SpreadsheetApp.getActiveSpreadsheet().rename(clientName + " Action Tracker");
moved_analytics_audit.SpreadsheetApp.getActiveSpreadsheet().rename(clientName + " Analytics Audit");
} else {
Logger.log('No Client Name Specified')
}
}
This function is finally triggered to add folder, subfolders, and sheets.
function test(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var databaseSheet = ss.getSheetByName("Clients");
var lastAvailableRow = databaseSheet.getLastRow();
const clientName = databaseSheet.getRange(lastAvailableRow, 2).getValue();
createClientAccountFolder(clientName, "[SHEET_ID]", "[DOC_ID]")
}
Main issues and solution:
If you just want to display the link, there's no need to add a formula, you can just set the value directly via Range.setValue(folderUrl).
If you want the displayed text to be different from the link itself, use the approach taken in DrDaNkk's answer, via RichTextValue.
You only want to set the value of a single cell (column K of the last row). Therefore, your range should not include multiple cells, as in your current code ("K2:K" + lastAvailableRow), but one, like "K" + lastRow.
In order to share the folder with an email address with edit access, use Folder.addEditor(emailAddress).
Code sample:
function addLinkAndShare() {
const customerRoot = DriveApp.getFolderById('folder_id');
const ss = SpreadsheetApp.getActiveSpreadsheet();
const databaseSheet = ss.getSheetByName("Clients");
const lastRow = databaseSheet.getLastRow();
const clientName = databaseSheet.getRange(lastRow, 2).getValue();
if (clientName) {
const mainFolder = customerRoot.createFolder(clientNameFolder);
const folderUrl = mainFolder.getUrl();
databaseSheet.getRange("K" + lastRow).setValue(folderUrl);
const clientEmail = databaseSheet.getRange("J" + lastRow).getValue();
mainFolder.addEditor(clientEmail);
/*
/ Creating subfolders, renaming, etc. (unrelated to your question)
*/
} else {
Logger.log('No Client Name Specified')
}
}
Other issues & improvements:
I'd suggest you to create the folder, add the folder URL and share it on the same script in which the data is appended to the sheet, right before doing that. This way, you can add all values at once and there's no need to retrieve the values that have just been set (e.g. clientName), and you'll reduce the amount of calls necessary (see Minimize calls to other services).
Since you didn't provide the code related to the data being appended to the sheet, though, I didn't add that to the code sample above.
I want you to realize you're attempting to add the same folder link to multiple cells in a sheet. I don't know if that's purposeful but I think you might want to change that range. Anyways, my suggested solution uses the RichTextValue object so you don't have to use a formula. You'll want to change
clients_sheet.setFormula('=HYPERLINK("' + mainFolder.getUrl() + '")');
to
var richText = SpreadsheetApp.newRichTextValue().setText("Folder URL").setLinkUrl(folder_url).build();
clients_sheet.setRichTextValue(richText);
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();
}
I have created a spreadsheet that contains quite a large amount of data.
The plan is to consolidate this data into a readable email to be sent out weekly, each specific row of data is its own email.
I tried going directly from sheets to email, but frankly it never quite looked right, plus the idea was to have a document template, where we could easily update the body without messing with code.
So I decided to write a email template in DOCS, set out a table, then have a script that copied the email template and updated the table with the row of data the script was looking at, then send it via email.
The code works great, but there is one little snag, the table never quite copies over to the email properly.
Below is are images of how the table is formatted in the email compared to the format in the template.
I just can not figure out how or why the format does not carry over.
I have also listed my code below, any help or advice on how I achieve the correct formatting would be appreciated.
UPDATE;
I have updated the question to show the code where we find the url of the document and convert to HTML,
var classArray=[];
//get html from Doc
var subject= row[30];
var forDriveScope = DriveApp.getStorageUsed(); //needed to get Drive Scope requested
var url = "https://docs.google.com/feeds/download/documents/export/Export?id="+newID+"&exportFormat=html";
var param = {
method : "get",
headers : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
muteHttpExceptions:true,
};
var html = UrlFetchApp.fetch(url,param).getContentText();
//docs uses css in the head, but gmail only takes it inline. need to move css inline.
//DOES NOT HANDLE HEADER CLASSES (eg h1, h2).
var headEnd = html.indexOf("</head>");
//get everything between <head> and </head>, remove quotes
var head = html.substring(html.indexOf("<head>")+6,headEnd).replace(/"/g,"");
//split on .c# with any positive integer amount of #s
var regex = /\.c\d{1,}/;
var classes = head.split(regex);
//get class info and put in an array index by class num. EG c4{size:small} will put "size:small" in classArray[4]
var totalLength = 0;
for(var i = 1; i < classes.length; i++){
//assume the first string (classes[0]) isn't a class definition
totalLength = totalLength + classes[i-1].length;
var cNum = head.substring(totalLength+2,head.indexOf("{",totalLength)); //totallength+2 chops off .c, so get what's between .c and {
totalLength = totalLength + 2 + cNum.length //add .c and the number of digits in the num
classArray[cNum] = classes[i].substring(1,classes[i].indexOf("}")); //put what's between .c#{ and } in classArray[#]
}
//now we have the class definitions, let's put it in the html
html = html.substring(headEnd+7,html.indexOf("</html>")); //get everything between <html> and </html>
var classMatch = /class=\"(c\d{1,} ){0,}(c\d{1,})\"/g
//matches class="c# c#..." where c#[space] occurs any number of times, even zero times, and c#[no space] occurs after it, exactly once
html = html.replace(classMatch,replacer); //replace class="c# c#..." with the definitions in classArray[#]
//make the e-mail!
GmailApp.sendEmail(row[31], subject, "HTML is not enabled in your email client. Sad face!", {
htmlBody: html,
});
function replacer(match){
var csOnly = match.substring(7,match.length-1); //class=" has 7 chars, remove the last "
var cs = csOnly.split(" "); //get each c#
var ret = "style=\""
for(var cCount = 0; cCount < cs.length; cCount++){
ret = ret + classArray[cs[cCount].substring(1)];
}
return ret+"\"";
}
})
}
The comments in the code says that Gmail can only use inline styling. That was true several years ago but currently Gmail allows to have a style tag inside a head tag. Considering this, the script could be much more simple that the one included in the question.
Below there is a script showing a sample that sends a Google Document content as the HTML body of an email message.
/**
* Get document as HTML
* Adapted from https://stackoverflow.com/a/28503601/1595451
*/
function getGoogleDocumentAsHTML(id) {
var forDriveScope = DriveApp.getStorageUsed(); //needed to get Drive Scope requested
var url = "https://docs.google.com/feeds/download/documents/export/Export?id=" + id + "&exportFormat=html";
var param = {
method: "get",
headers: { "Authorization": "Bearer " + ScriptApp.getOAuthToken() },
muteHttpExceptions: true,
};
var html = UrlFetchApp.fetch(url, param).getContentText();
return html;
}
/**
* Send the content of a Google Document as the HTML body of a email message
*/
function sendEmail(){
const url = /* add here the URL of your Google Document */;
const id = url.match(/[^\/]{44}/)[0];
const doc = getGoogleDocumentAsHTML(id);
const head = doc
.replace(/<meta[^>]+?>/g,'') // get rid of the meta tags
.match(/<head.+?<\/head>/)[0];
const body = doc.match(/<body[^>]+?>.+<\/body>/)[0];
const htmlBody = [head,body].join('\n');
MailApp.sendEmail({
to: /*add here the recipient email address */,
subject: /*add here the email subject */,
htmlBody: htmlBody
})
}
NOTE: You might want to clear the class of the body tag to avoid the margins set for it.
My Sheet cell colors are changed automatically based on results values. Need to Send Send this data range in email Body... Code is running good, but do not pick colors of cells as displayed in below image. Help me set the code to copy and send body email as the data shown below.
Actual Sheet Display:
Here is the script code copying data from Google Sheet and Sending to email Body (using an html template) you can view here.
function getEmailHtml(stockData) {
var htmlTemplate = HtmlService.createTemplateFromFile("Template.html");
htmlTemplate.stocks = stockData;
var htmlBody = htmlTemplate.evaluate().getContent();
return htmlBody;
}
function sendEmail() {
var stockData = getData();
var body = getEmailText(stockData);
var htmlBody = getEmailHtml(stockData);
MailApp.sendEmail({
to: "email#email.com",
subject: "Operations - Today's Job Details",
body: body,
htmlBody: htmlBody
});
}
function getEmailText(stockData) {
var text = "";
stockData.forEach(function(stock) {
text = text + stock.name + "\n" + stock.ticker + "\n" + stock.price + "\n-----------------------\n\n";
});
return text;
}
/**
* #OnlyCurrentDoc
*/
function getData() {
var values = SpreadsheetApp.getActive().getSheetByName("Email").getRange("Stocks").getDisplayValues();
values.shift(); //remove headers
var stocks = [];
values.forEach(function(value) {
var stock = {};
stock.sr = value[0];
stock.job = value[1];
stock.work = value[2];
stock.worked = value[3];
stock.time = value[4];
stocks.push(stock);
})
//Logger.log(JSON.stringify(stocks));
return stocks;
}
I believe your goal and your current situation as follows.
You want to set the background color of the rows of Spreadsheet to the rows of HTML table.
In your Spreadsheet, each row has one background color.
In this case, I would like to propose to retrieve the background colors of each row of the Spreadsheet, and those are set to the value of style of tr using the template. When your script is modified, it becomes as follows.
Modified script:
Please modify the function of getEmailHtml of Google Apps Script as follows.
function getEmailHtml(stockData) {
var range = SpreadsheetApp.getActive().getSheetByName("Email").getRange("Stocks");
var colors = range.getBackgrounds().map(([a]) => a);
colors.shift();
var htmlTemplate = HtmlService.createTemplateFromFile("Template.html");
htmlTemplate.stocks = stockData;
htmlTemplate.colors = colors;
var htmlBody = htmlTemplate.evaluate().getContent();
return htmlBody;
}
And also, please modify your HTML template as follows.
From:
<? for(var i = 0; i < stocks.length; i++) { ?>
<tr style="height:21px">
To:
<? for(var i = 0; i < stocks.length; i++) { ?>
<tr style="height:21px;background-color:<?= colors[i] ?>;">
Note:
In this modification, the background colors are retrieved from the 1st column of your range of SpreadsheetApp.getActive().getSheetByName("Email").getRange("Stocks"). When you want to change this, please modify above script.
Reference:
getBackgrounds()
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