Send emails from two distinct columns using data in google spreadsheets - javascript

Good morning.
Very new coder with little background. I need to merge data from a google spreadsheet into an email, without using an add-on. I borrowed this code from a site and I'm able to produce an email but it will only pull from the first email address column. I need to send an email to both the manager and director. Their email address will be stored in two separate columns with unique labels. I can't change the spreadsheet data as the spreadsheet is storing responses pulled from a survey form that is already in progress (example column layout below):
Name / Email Address / Director Name / Director Email Address / Response 1 / Response 2 / etc...
Everything I've researched will send an email from one column, but not two, or a "cc".
Below is the borrowed code. Would very much appreciate any help on how to modify the code to send the "response" data to both the Manager and Director in one email.
kind regards,
KA
function sendEmails() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dataSheet = ss.getSheets()[0];
var dataRange = dataSheet.getRange(2, 1, dataSheet.getMaxRows() - 1, 15);
var templateSheet = ss.getSheets()[1];
var emailTemplate = templateSheet.getRange("A1").getValue();
// Create one JavaScript object per row of data.
var objects = getRowsData(dataSheet, dataRange);
// For every row object, create a personalized email from a template and send
// it to the appropriate person.
for (var i = 0; i < objects.length; ++i) {
// Get a row object
var rowData = objects[i];
// Generate a personalized email.
// Given a template string, replace markers (for instance ${"First Name"}) with
// the corresponding value in a row object (for instance rowData.firstName).
var emailText = fillInTemplateFromObject(emailTemplate, rowData);
var emailSubject = "Data Survey";
MailApp.sendEmail(rowData.emailAddress, emailSubject, emailText);
}
}
// Replaces markers in a template string with values define in a JavaScript data object.
// Arguments:
// - template: string containing markers, for instance ${"Column name"}
// - data: JavaScript object with values to that will replace markers. For instance
// data.columnName will replace marker ${"Column name"}
// Returns a string without markers. If no data is found to replace a marker, it is
// simply removed.
function fillInTemplateFromObject(template, data) {
var email = template;
// Search for all the variables to be replaced, for instance ${"Column name"}
var templateVars = template.match(/\$\{\"[^\"]+\"\}/g);
// Replace variables from the template with the actual values from the data object.
// If no value is available, replace with the empty string.
for (var i = 0; i < templateVars.length; ++i) {
// normalizeHeader ignores ${"} so we can call it directly here.
var variableData = data[normalizeHeader(templateVars[i])];
email = email.replace(templateVars[i], variableData || "");
}
return email;
}
//////////////////////////////////////////////////////////////////////////////////////////
//
// The code below is reused from the 'Reading Spreadsheet data using JavaScript Objects'
// tutorial.
//
//////////////////////////////////////////////////////////////////////////////////////////
// getRowsData iterates row by row in the input range and returns an array of objects.
// Each object contains all the data for a given row, indexed by its normalized column name.
// Arguments:
// - sheet: the sheet object that contains the data to be processed
// - range: the exact range of cells where the data is stored
// - columnHeadersRowIndex: specifies the row number where the column names are stored.
// This argument is optional and it defaults to the row immediately above range;
// Returns an Array of objects.
function getRowsData(sheet, range, columnHeadersRowIndex) {
columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;
var numColumns = range.getEndColumn() - range.getColumn() + 1;
var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);
var headers = headersRange.getValues()[0];
return getObjects(range.getValues(), normalizeHeaders(headers));
}
// For every row of data in data, generates an object that contains the data. Names of
// object fields are defined in keys.
// Arguments:
// - data: JavaScript 2d array
// - keys: Array of Strings that define the property names for the objects to create
function getObjects(data, keys) {
var objects = [];
for (var i = 0; i < data.length; ++i) {
var object = {};
var hasData = false;
for (var j = 0; j < data[i].length; ++j) {
var cellData = data[i][j];
if (isCellEmpty(cellData)) {
continue;
}
object[keys[j]] = cellData;
hasData = true;
}
if (hasData) {
objects.push(object);
}
}
return objects;
}
// Returns an Array of normalized Strings.
// Arguments:
// - headers: Array of Strings to normalize
function normalizeHeaders(headers) {
var keys = [];
for (var i = 0; i < headers.length; ++i) {
var key = normalizeHeader(headers[i]);
if (key.length > 0) {
keys.push(key);
}
}
return keys;
}
// Normalizes a string, by removing all alphanumeric characters and using mixed case
// to separate words. The output will always start with a lower case letter.
// This function is designed to produce JavaScript object property names.
// Arguments:
// - header: string to normalize
// Examples:
// "First Name" -> "firstName"
// "Market Cap (millions) -> "marketCapMillions
// "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored"
function normalizeHeader(header) {
var key = "";
var upperCase = false;
for (var i = 0; i < header.length; ++i) {
var letter = header[i];
if (letter == " " && key.length > 0) {
upperCase = true;
continue;
}
if (!isAlnum(letter)) {
continue;
}
if (key.length == 0 && isDigit(letter)) {
continue; // first character must be a letter
}
if (upperCase) {
upperCase = false;
key += letter.toUpperCase();
} else {
key += letter.toLowerCase();
}
}
return key;
}
// Returns true if the cell where cellData was read from is empty.
// Arguments:
// - cellData: string
function isCellEmpty(cellData) {
return typeof(cellData) == "string" && cellData == "";
}
// Returns true if the character char is alphabetical, false otherwise.
function isAlnum(char) {
return char >= 'A' && char <= 'Z' ||
char >= 'a' && char <= 'z' ||
isDigit(char);
}
// Returns true if the character char is a digit, false otherwise.
function isDigit(char) {
return char >= '0' && char <= '9';
}

According to the documentation : Mail App, the recipent represents the addresses of the recipients, separated by commas
So in order to send to several recipents, just make a string separated by comma:mike#example.com, mike2#example.com
You could pick the rows you want and do something like this:
MailApp.sendEmail(rowData[0].emailAddress + ',' + rowData[1].emailAdress, emailSubject, emailText);

Related

Check whether Google Sheets cells contain values using Google Apps Script

In google app scripts, I am pulling in data (all strings) from a google sheet and then trying to split up the array into separate columns excluding the first row (header row). The data has an arbitrary amount of rows and columns. "projects" are the headers and "tasks" is the data underneath each header.
// load in task names
function LoadTasks() {
var sheet = ss.getSheetByName("Task List");
sheet.activate();
var allTasks = sheet.getDataRange().getValues();
return allTasks;
}
function Analysis(project, tasks) {
var sheet = ss.getSheetByName(project);
sheet.activate;
var taskLength = tasks.length;
var sheetLength = (taskLength*4) + 10;
Logger.log("The Project is " + project + " with task length of " + taskLength + " and task data of: " + tasks);
}
taskData = LoadTasks();
numProjects = taskData[0].length;
// load all project names into single array
for (i = 0; i < numProjects; i++) {
projectNames[i] = taskData[0][i];
}
for (i = 0; i < numProjects; i++) {
project = projectNames[i];
j = 1;
while (taskData[j][i] != null) {
tasks[j-1] = taskData[j][i];
j++;
}
Analysis(project, tasks)
tasks = [];
}
In the very last while loop, how can I check to see if the array value I'm looking at holds a value (does not contain a null, undefined, or blank). My current method gives me the error: Cannot read property "0.0" from undefined. (line 91, file "Code")
***Line 91: while (taskData[j][i] != null) {
The method getValues() returns a double array of values which can be numbers, strings, or Date objects. There are no "null" or "undefined" among the values. Blank cells are represented as empty strings "" and you can detect them by comparison to "".
That said, one should control the index bounds explicitly:
while (j < taskData.length && taskData[j][i] !== "") {
tasks[j-1] = taskData[j][i];
j++;
}

Check for Partial Match in an Array

I have a JavaScript array that contains some words that cannot be used when requesting user accounts to be created.
I am trying to loop over the accounts requested and check them against a word filter. If they contain any of the words, the value is moved to an array of "Invalid Accounts".
// Create our arrays
var blacklist = ["admin", "webform", "spoof"];
var newAccounts = ["admin1#google.com", "interweb#google.com", "puppy#google.com"];
var invalidAccounts = [];
// I need to check if any of the new accounts have matches to those in the blacklist.
// admin1#google.com would need to be pushed into the invalidAccounts array because it
// contains the word admin. Although interweb contains the word web, it does not contain
// the full word, webform, so should be ignored.
// Loop over the blacklist array
for(var x = 0; x < blacklist.length; x++){
if(blacklist[x].indexOf(newAccounts) > -1){
alert(blacklist[x] + " is in the blacklist array");
// Push the newAccounts value into the invalidAccounts array since it contains
// a blacklist word.
} else {
alert('no matches');
}
}
What do I need to change in the above code to have it match the partial strings such as mentioned?
Fiddle of above code: https://jsfiddle.net/1qwze81o/
You probably won't need to use all of this but it should be helpful none the less:
var blacklist = ["admin", "webform", "spoof"];
var newAccounts = ["admin1#google.com", "interweb#google.com", "puppy#google.com"];
var invalidAccounts = [];
// transform each email address to an object of the form:
// { email: string, valid: boolean }
var accountObjects = newAccounts.map(function (a) {
return { email: a, valid: true };
});
// loop over each account
accountObjects.forEach(function (account) {
// loop over the blacklisted terms
blacklist.forEach(function (blacklisted) {
// check to see if your account email address contains a black listed term
// and set the valid property accordingly
account.valid = account.email.search(blacklisted) === -1;
});
});
// filter accountObjects, validAccounts will now contain an array of valid objects
var validAccounts = accountObjects.filter(function (a) {
return a.valid;
});
// back to the original type of a string array
var validEmailAddresses = validAccounts.map(function (a) {
return a.email;
});
A solution using javascript array native functions:
var invalidAccounts = newAccounts.filter(function(account){ // we need to filter accounts
return blacklist.some(function(word){ // and return those that have any of the words in the text
return account.indexOf(word) != -1
})
});
More info on: Array.filter and
Array.some
We need two loops to achieve this:
something like below:
// Loop over the blacklist array
for(var j = 0; x < newAccounts.length; j++){
for(var x = 0; x < blacklist.length; x++){
if(newAccounts[j].indexOf(blacklist[x]) > -1){
alert(blacklist[x] + " is in the blacklist array");
// Push the newAccounts value into the invalidAccounts array since it contains a blacklist word.
}else{
alert('no matches');
}
}
}
I fixed some of the things...
for (var x = 0; x < newAccounts.length; x++) // loop through new accounts
{
// for every account check if there is any match in blacklist array
for (var y = 0; y < blacklist.length; y++)
{
// if there is match do something
if (newAccounts[x].indexOf(blacklist[y]) > -1)
{
alert(newAccounts[x] + " is in the blacklist array");
break;
}
}
}
Here is the fiddle: https://jsfiddle.net/swaprks/n1rkfuzh/
JAVASCRIPT:
// Create our arrays
var blacklist = ["admin", "webform", "spoof"];
var newAccounts = ["admin1#google.com", "interweb#google.com", "puppy#google.com"];
var invalidAccounts = [];
for(var x = 0; x < newAccounts.length; x++){
for(var y = 0; y < blacklist.length; y++){
if( newAccounts[x].indexOf(blacklist[y]) > -1){
alert(blacklist[x] + " is in the blacklist array");
// Push the newAccounts value into the invalidAccounts array since it contains
// a blacklist word.
invalidAccounts.push(newAccounts[x]);
}else{
alert('no matches');
}
}
}

Writing an array to a spreadsheet

I've written some script that reads an array (of Responses - multiple rows and columns) from a spreadsheet sheet, extracts data from each row into one or two lines (Entries) which are pushed into a 'main' array, and then writes that 'main' array to a different sheet. Each row of the Responses can contain one or two Entries - shown by a marker value.
Works well except that only the last Entry is pasted, the correct number of rows, to the second sheet.
Somewhere I am not placing the value of each object into the Entry array, just a reference to the value, but cannot see where I have gone wrong.
The spreadsheet is at
https://docs.google.com/spreadsheets/d/1x-HO7KIAQ_s7q55opTd2LRpRRo95S1CqeH193RalWS0/pubhtml
and my script is as follows:
function transferResponses() {
// establish sheets
var aFile = SpreadsheetApp.getActiveSpreadsheet();
var aResponseSh = aFile.getSheetByName("Form responses 1");
var aBaseSh = aFile.getSheetByName("Base");
// check number of response
var numResponses = aResponseSh.getLastRow() - 1;
// put responses in an array
var aResponses = aResponseSh.getSheetValues(2, 1, numResponses, 23);
// define other variables to use
var oneEntry = []; // array for a single entry
var aEntries = []; // array of all entries
var aNumber; // increment part of ID
// counters, etc
var iRes;
var iCol;
var iEntry = 0;
for (iRes = 0; iRes < numResponses; iRes++) {
aNumber = iEntry + 101;
oneEntry[0] = "A" + aNumber.toString(); // form and load entry ID
for (iCol = 1; iCol < 12; iCol++) { // load name, surname, address 1, address 2, phone, email
oneEntry[iCol] = aResponses[iRes][iCol]; // and first entry title, classification, size, description, price
}
oneEntry[12] = aResponses[iRes][18]; // load novice status
oneEntry[13] = aResponses[iRes][21]; // load date of entry
oneEntry[14] = aResponses[iRes][22]; // load method of payment
aEntries.push(oneEntry.valueOf()); // push entry (oneEntry) into main aray (aEntries)
iEntry = iEntry + 1; // increment single entry counter
if (aResponses[iRes][12] === "Add another entry") { // check for a second entry on response
aNumber = iEntry + 101;
oneEntry[0] = "A" + aNumber.toString(); // form and load entry ID
for (iCol = 1; iCol < 7; iCol++) { // load name, surname, address 1, address 2, phone, email
oneEntry[iCol] = aResponses[iRes][iCol]; // and first entry title, classification, size, description, price
}
for (iCol = 7; iCol < 12; iCol++) { // and second entry title, classification, size, description, price
oneEntry[iCol] = aResponses[iRes][iCol + 6];
}
oneEntry[12] = aResponses[iRes][18]; // load novice status
oneEntry[13] = aResponses[iRes][21]; // load date of entry
oneEntry[14] = aResponses[iRes][22]; // load method of payment
aEntries.push(oneEntry.valueOf()); // push entry (oneEntry) into main aray (aEntries)
iEntry = iEntry + 1; // increment single entry counter
}
}
lastRow = aBaseSh.getLastRow();
if (lastRow > 1) { // clear all existing data on base sheet
aBaseSh.deleteRows(2, lastRow);
}
aBaseSh.getRange(2, 1, aEntries.length, aEntries[0].length).setValues(aEntries); // paste main array (aEntries)
}
Try this in every Array after each the equals:
oneEntry[iCol] = aResponses[iRes][iCol].slice(0);
or if only one string:
oneEntry[iCol] = aResponses[iRes][iCol].valueOf;
Also, you question is not specific, Writing an array to a spreadsheet is a pretty generic one, and has nothing to do with your error, wich is caused by Array referencing instead of cloning, your question should also be minimized while still reproducing the error.

Google sheets: How do I get my custom function to refresh automatically?

I have written a Google Sheet application for myself that (among other things) has a schedule for consultants to work on projects. I have written a custom function that displays summary information from the calendar; for example:
Tubb, Ernest|Project 1
Tubb, Ernest|Project 2
Perkins, Carl|Project 1
I wrote the function because the mess of FILTER() functions I was using was incomprehensible; and the javascript required to do the same thing is relatively straightforward.
What I find however, is that when the underlying data changes, the cells written by my function are NOT recalculated the way they were with the FILTER() function. How can I program some sort of listener that makes my function's output be refreshed when the underlying data changes? Do I have to do this by hand with the onEdit() function?
And here is my function:
// Global Constants - these will change each quarter based on the calendar structure.
var CALENDAR_QUARTER_START = "F"
var CALENDAR_QUARTER_END = "CS"
// Generate a nice 2-column display of Consultant and Project(s) they are scheduled on,
// based on the consultant names and calendar codes in the Calendar tab.
// "billable" parameter specifies the return of only uppercase activities (true) or all activities (false)
function fortifyConsultantsAndProjects( billable ){
// Resolve arguments
var billable = arguments[0];
if (arguments[0] == undefined) billable = true;
var calendar = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calendar");
var consultants = calendar.getRange( "A8:A" ).getValues();
var ret = [];
var row = 0;
for ( var i=0; i<consultants.length; i++ ){
var consultant = consultants[i].toString();
var projects = fortifyGetProjectsForConsultant( consultant, billable );
for (var j=0; j < projects.length; j++ ) {
ret.push( [] );
ret[row][0] = consultant;
ret[row][1] = projects[j];
row++;
}
}
return ret;
}
function fortifyGetProjectsForConsultant( consultant, billable ){
// Resolve arguments
var consultant = arguments[0];
if (arguments[0] == undefined) consultant = "Held, Doug";
var billable = arguments[1];
if (arguments[1] == undefined) billable = true;
// Get the range of consultants defined in Column A of Calendar tab.
var calendar = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calendar");
var consultants = calendar.getRange( "A8:A" ).getValues();
// Identify which Calendar row contains the specified consultant's schedule.
var row;
for ( var i=0; i< consultants.length; i++){
//Browser.msgBox( "compare " + consultant + " and " + consultants[i] );
if ( consultants[i].toString() == consultant ){
row = i+8;
break;
}
}
// Obtain all the consultant's schedulings. Contains many duplicates, so sort.
var projects = calendar.getRange( CALENDAR_QUARTER_START + row + ":" + CALENDAR_QUARTER_END + row + "" ).getValues()[0].sort();
// Iterate through the sorted project codes, removing duplicates and blanks
var ret = [];
var row = 0;
var prev = "";
for ( var i = 0; i< projects.length; i++ ){
var temp = projects[i].toString();
if (temp != "" && temp != prev ){
// Resolve whether to return each row based on project billability (uppercase)
if ( temp.toUpperCase() == temp || billable == false ){
ret[row] = temp;
row++;
}
prev = temp;
}
}
return ret;
}
Well since nobody ever answered, I will explain how I solved the problem.
Instead of a zero-argument function parameter list, I receive all of the ranges (for instance the range Calendar!A8:A that you see hard coded here). Then any time data in the parameter ranges changes, the function is invoked again. This does occasionally cause some sort of runtime error in Google sheets - on update, I sometimes get "the document is not available" and an instruction to reload the sheet.
When a range is received as a parameter, that data is received byvalue as a 2 dimensional array of cell values -- the same as the array returned from Range.getValues().

Javascript Split Array and assign values to variables from NZBMatrix API

Not sure if any of you guys/girls out there that uses the NZBMatrix website API..
In short what I'm trying to do is build an Adobe Air Application,
using JavaScript, AJAX to connect to the API with a search query, this is all good.
When i receive the "request.responseText" back from the API with the 5 results
(can only be 5) I'm having trouble with the JavaScript split function trying to split them all out...
the return string is returned as follows:
NZBID:444027;
NZBNAME:test result 1;
LINK:nzbmatrix.com/nzb-details.php?id=444027&hit=1;
SIZE:1469988208.64;
INDEX_DATE:2009-02-14 09:08:55;
USENET_DATE:2009-02-12 2:48:47;
CATEGORY:TV > Divx/Xvid;
GROUP:alt.binaries.test;
COMMENTS:0;
HITS:174;
NFO:yes;
REGION:0;
|
NZBID:444028;
NZBNAME:another test;
LINK:nzbmatrix.com/nzb-details.php?id=444028&hit=1;
SIZE:1469988208.64; = Size in bytes
etc..etc..
the first Array should split each set of results using |
assign those 5 results to a new array.
the 2nd Array should split each value using :
assign those 12 results to new variables
ie: var nzbidtxt = array1[0]; which would echo like:
document.write(nzbidtxt); // ie: print "NZBID:"
the 3rd Array should split each variable from ;
assign those 12 values to the newly created array
ie: var nzbidValue = array2[0]; which would echo like:
document.write(nzbValue); // ie: print "444027"
so using both arrays I can display a listing of the posts returned..
in a nice usable format..
nzbid: 444027 // this will be used for direct download
nzbName: the name of the nzb
etc..etc..
the function i have been working on is below:
function breakNzbUrlResponse(text)
{
var place = new Array;
var place2 =new Array;
var place3 =new Array;
place[0] = text.indexOf('|');
place2[0] = text.indexOf(':');
place3[0] = text.indexOf(';');
var i = 1;
while(place[i-1] > 0 || i==1) {
place[i] = text.indexOf('|',place[i-1]+1);
place2[i] = text.indexOf(':',place2[i-1]+1);
if(place2[i] == -1)
{
place2[i] = text.length;
}
i++;
}
i=1;
var vars = new Array;
var values = new Array;
var retarray = new Array;
vars[0] = text.substr(0,place[0]);
values[0] = text.substr((place[0]+1),((place2[0]-place[0])-1));
retarray[vars[0]] = values[0];
while(i < (place.length-1) || i==1)
{
vars[i] = text.substr((place2[i-1]+1),((place[i]-place2[i-1])-1));
values[i] = text.substr((place[i]+1),((place2[i]-place[i])-1));
//alert('in loop\r\nvars['+i+'] is: '+vars[i]+'\r\nvalues['+i+'] is: '+values[i]);
retarray[vars[i]] = values[i];
i++;
}
return retarray;
}
This feels and looks like a very long winded process for this type..
all I want to do is basically assign a new variable to each return type
ie
var nzbid = array3[0];
which when split would reference the first line of the return string, NZBID:444027; where the value for NZBID would be 44027..
bit of a book going on, but the more info the better i suppose.
Thanks
Marty
You could probably cut out a significant number of lines of code by further utilizing split() instead of the manual dissections of the entries and using multidimensional arrays instead of repeatedly creating new arrays.
The logic would be:
ResultsArray = split by "|"
FieldArray = Each element of FieldArray split by ";"
ValueArray = Each element of FieldArray split by ":"
2 years later, it's sad that NZBMatrix is still using this horrible format. Here is how you can parse it.
//used to hold temporary key/value pairs
var tempKV = {};
//used to hold the search results
this.searchResults = [];
//The unformatted search results arrive in inResponse
//Remove whitespace and newlines from the input
inResponse = inResponse.replace(/(\r\n|\n|\r)/gm,"");
//search entries are delimited by |
var results = inResponse.split("|");
for(var i = 0; i < results.length; i++){
//key:value pairs in each search result are dlimited by ;
var pair = results[i].split(";");
for(var j = 0; j < pair.length; j++){
//keys and values are delimited by :
var kv = pair[j].split(":");
//normal key:value pairs have a length of 2
if(kv.length == 2){
//make sure these are treated as strings
//tempKV["key"] = "value"
tempKV["" + kv[0]] = "" + kv[1];
}
//Else we are parsing an entry like "http://" where there are multiple :'s
else if(kv.length > 2){
//store the first chunk of the value
var val = "" + kv[1];
//loop through remaining chunks of the value
for(var z = 2; z < kv.length; z++){
//append ':' plus the next value chunk
val += ":" + kv[z];
}
//store the key and the constructed value
tempKV["" + kv[0]] = val;
}
}
//add the final tempKV array to the searchResults object so long
//as it seems to be valid and has the NZBNAME field
if(tempKV.NZBNAME){
this.searchResults[i] = tempKV;
}
//reset the temporary key:value array
tempKV = {};
}
//all done, this.searchResults contains the json search results

Categories

Resources