Getting "undefined" when trying to get values from spreadsheet - javascript

I'm getting an "undefined" error when I try to get the value from a cell in a spreadsheet. The thing is that if I execute the same command for a different cell I get the value in that cell. The only difference between those 2 cells is the way the value is produced.
The value in the cell that show correctly is produced directly from the Google Form associated with that spreadsheet. The value that doesn't show when called, is produced from a script I created in the Google Form.
Script for the Form (triggered on form submit):
// This code will set the edit form url as the value at cell "C2".
function assignEditUrls() {
var form = FormApp.getActiveForm();
var ss = SpreadsheetApp.openById("my-spreadsheet-id")
var sheet = ss.getSheets()[0];
var urlCol = 3; // column number where URL's should be populated; A = 1, B = 2 etc
var formResponses = form.getResponses();
for (var i = 0; i < formResponses.length; i++) {
var resultUrl = formResponses[i].getEditResponseUrl();
sheet.getRange(2 + i, urlCol).setValue(resultUrl);
}
SpreadsheetApp.flush();
}
Table (changed to HTML)
<table>
<tr> <!-- Row 1 -->
<td>Timestamp</td> <!-- A1 -->
<td>Name</td> <!-- B1 -->
<td>Edit form URL</td> <!-- C1 -->
</tr>
<tr> <!-- Row 2 -->
<td>5/26/2015 14:04:09</td> <!-- A2: this value came from the form submittion-->
<td>Jones, Donna</td> <!-- B2: this value came from the form submittion-->
<td>https://docs.google.com/forms/d/1-FeW-mXh_8g/viewform?edit2=2_ABaOh9</td> <!-- C2: this value came from the the script in the form -->
</tr>
</table>
Script in Spreadsheet (Triggered on form submit)
function onFormSubmit(e) {
// This script will get the values from different cells in the spreadsheet
// and will send them into an email.
var name = e.range.getValues()[0][1]; // This will get the value from cell "B2".
var editFormURL = e.range.getValues()[0][2]; // This will get the value from cell "C2".
var email = 'my-email#university.edu';
var subject = "Here goes the email subject."
var message = 'This is the body of the email and includes'
+ 'the value from cell "B2" <b>'
+ name + '</b>. This value is retrieved correctly.'
+ '<br>But the value from cell "C2" <b>'+ editFormURL
+ '</b> show as "undefined".';
MailApp.sendEmail(email, subject, message, {htmlBody: message});
}
The email looks like this:
Sented by: my-email#university.edu
Subject: Here goes the email subject.
Body:
This is the body of the email and includes the value from cell "B2" Jones, Donna. This value is retrieved correctly.
But the value from cell "C2" undefined show as "undefined".
Question:
What am I doing wrong?

You've most likely got a race condition in play.
A user submits a form. This is our prime event.
Upon form submission, all triggers that are associated with the event are fired.
assignEditUrls() in the form script, and
onFormSubmit() in the spreadsheet script.
If you had other scripts set up for this event, they would also trigger. The complication here is that all those triggers are fired independently, more-or-less at the same time, but with no guaranteed order of execution. The spreadsheet trigger MIGHT run BEFORE the form trigger! So that's one problem.
Each trigger will receive the event information in the format specific to their definition. (See Event Objects.) Since C2 is not actually part of the form submission, it won't be in the event object received by the spreadsheet function. That's your second problem, but since you know the offset of the value relative to the form input, you can use range.offset() to get it.
An additional wrinkle has to do with the way that Documents and Spreadsheets are shared; each separate script invocation will receive its own copy of the Spreadsheet, which is synchronized with other copies... eventually. Changes made to the spreadsheet by one script will not be immediately visible to all other users. And that makes three problems.
What to do?
You could try to coordinate operations of the two related trigger functions. If they're in the same script, the Lock Service could help with this.
You could have just one trigger function to perform both operations.
Or you could make the spreadsheet function tolerant of any delays, by having it wait for C2 to be populated. This snippet would do that...
...
var editFormURL = null;
var loop = 0;
while (!editFormURL) {
editFormURL = e.range.offset(0,2).getValue(); // This will get the value from cell "C2".
if (!editFormURL) {
// Not ready yet, should we wait?
if (loop++ < 10) {
Utilities.sleep(2000); // sleep 2 seconds
}
else throw new Error( 'Gave up waiting.' );
}
}
// If the script gets here, then it has retrieved a value for editFormURL.
...
One bonus problem: since you're using getValues(), with the plural s, you are retrieving 2-dimensional arrays of information. You're not seeing a problem because when you treat those values like a string, the javascript interpreter coerces the array into the string you've wished for. But it is still a problem - if you want a single value, use getValue().

Get the correct row, then hard code the column with your URL:
var ss = SpreadsheetApp.openById("my-spreadsheet-id")
var sheet = ss.getSheets()[0];
var rowForLookup = e.range.getRow();
var columnOfUrl = 24; //Column X
var theUrl = sheet.getRange(rowForLookup, columnOfUrl).getValue();

So this code is what I finally got after implementing your recommendations. Thanks to Sandy Good and Mogsdad.
function onFormSubmit(e) {
Logger.log("Event Range: " + e.range.getA1Notation());
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var startRow = e.range.getRow();
var startCol = 1;
var numRows = 1;
var numColumns = sheet.getLastColumn();
var dataRange = sheet.getRange(startRow, startCol, numRows, numColumns);
var data = dataRange.getValues();
Logger.log("Data Range: " + dataRange.getA1Notation());
for (var i = 0; i < data.length; ++i) {
var column = data[i];
var name = column[4];
var editFormURL = null;
var loop = 0;
while (!editFormURL) {
editFormURL = column[23];
if (!editFormURL) {
// Not ready yet, should we wait?
if (loop++ < 10) {
Utilities.sleep(3000); // sleep 2 second
}
else throw new Error( 'Gave up waiting.' );
}
}
var email = 'my-email#university.edu';
var subject = "Subject here";
var message = 'Some text about ' + name + '.' +
'<br><br>Please view this link: ' +
+ editFormURL;
MailApp.sendEmail(email, subject, message, {htmlBody: message});
}
}

Related

What is the correct structure of a 2D array in GAS that is used to create a CheckboxgridItem response in a Google Form

The previous "answer" that I have been referred to does not solve this problem. I tried that technique before I posted here. That technique did not work and I received the same error as the one I'm receiving now.
I am trying to create a Google Apps Script that will allow me to use previous data from a Form and "resubmit" the data into a Form. The use case is that I will have teachers who change Form questions after they have received submissions from sections of their class. The change will cause the previous data to no longer be included in the Form response summary page or the individual responses. Instead the data is hidden from view and only accessible through downloading the csv of the responses or the data found in the linked Sheet (provided the teacher linked the Sheet before editing the Form). The current work around is to use the Form's Pre-Filled-URL feature and then create a URL for each row of data through the use of formulas in the Sheet. The teacher must then manually click on each link to enter the data back into the Form. This is not only time consuming but many of the teachers cannot create a formula that will create the individual url links.
To solve this, I'm creating a script that I can use with these teachers when this occurs. I have everything figured out except for the grid and time items. Here's a stripped down version of the script that applies only to a checkbox_grid question.
The data is in one column (B1:B) like this:
1X2 Grid [R1]
C1, C2
C1
C2
C1, C2
In the script, I've been able to create an array for each cell that looks like:
array[0] = [['C1'].['C2']]
array[1] = [['C1'],['']]
array[2] = [[''],['C2']]
array[3] = [['C1'].['C2']]
The following error is being returned:
Exception: Wrong number of responses: 2. Expected exactly: 1.
Also, while researching this further, I found the following in the developers site:
For GridItem questions, this returns a String[] array in which the answer at index n corresponds to the question at row n + 1 in the grid. If a respondent did not answer a question in the grid, that answer is returned as ''.
For CheckboxGridItem questions, this returns a String[][] array in which the answers at row index n corresponds to the question at row n + 1 in the checkbox grid. If a respondent did not answer a question in the grid, that answer is returned as ''.
What is the correct way to structure the array so that GAS will accept it?
Code found in sample file is here:
//Global Variables for Spreadsheet
const ss = SpreadsheetApp.getActiveSpreadsheet();
var dataSheet = ss.getSheetByName('Data');
var lastRow = dataSheet.getLastRow();
var lastCol = dataSheet.getLastColumn();
var sheetHeader = dataSheet.getRange('1:1');
var sheetTitles = sheetHeader.getValues();
var error = 'None';
var cellValue = [];
//Global Variables for Form
var form = FormApp.openByUrl(ss.getFormUrl());
var formResponses = form.getResponses();
var formCreate = form.createResponse();
var formItems = form.getItems();
//sendValues() calls the setResponses() to set the values of each form response.
// The function then submits the forms and clears the responses.
// Flush is used to make sure the order of the code is retained.
//------------------ New Variables -------------------------------
// r = row
function sendValues() {
for (r = 2; r <= lastRow; r++) {
setResponses(r);
//formCreate.submit();
console.log('submitted');
formCreate = form.createResponse();
SpreadsheetApp.flush();
}
}
//setResponses(r) sets the response for each cell of the data sheet.
//calls gridResponse(cell,i) if the question type is either a checkbox grid or multiple choice grid question.
//----------------------- New Variables ---------------------------
// c = column
// i = item response
// ssHeaderValue = Values of the Row 1 in the Data Sheet (dataSheet)
// cell = Value of each cell in dataSheet
// cellValue = Converts commas in cell variable to ;
// formItem = each item from all of the Form items
// fHeaderValue = Value of each "header" (Title) of each Form value
// itemAs = item set to correct type
// itemResponse = created response
function setResponses(r) {
for (c = 2; c <= lastCol; c++) {
for (i = 0; i < formItems.length; i++) {
var ssHeaderValue = dataSheet.getRange(1, c).getValue();
var itemType = formItems[i].getType();
var cell = dataSheet.getRange(r, c).getValue();
gridResponse(cell, i);
var formItem = formItems[i];
var fHeaderValue = formItem.getTitle();
var itemResponse = formItem
.asCheckboxGridItem()
.createResponse(cellValue);
}
//ERROR HERE: formCreate.withItemResponse(itemResponse);
}
}
//checkboxGridResponse(cell,i) makes an array of cellValue that can be used in CHECKBOX_GRID item.
//--------------------- New variables ------------------------------
// z = loop counter
// gridColumns = number of possible responses in the Form
// cellValueLength = number of responses in the cell data
// cellColumns = cell data split into separate values
// cellValue = value to be returned
// arr = temporary array for making cellValue into a 2D array
function gridResponse(cell, i) {
var gridColumns = formItems[i].asCheckboxGridItem().getColumns();
var cellValueLength = cell.split(',').length;
var cellColumns = cell.split(',');
for (z = 0; z < cellValueLength; z++) {
console.log(
'cellColumns ' +
z +
' = ' +
cellColumns[z] +
'; gridColumns ' +
z +
' = ' +
gridColumns[z]
);
var arr = [gridColumns[z] == cellColumns[z] ? gridColumns[z] : null];
cellValue.push(arr);
}
console.log(cellValue);
console.log('cellValue[0][0] = ' + cellValue[0][0]);
console.log('cellValue[0][1] = ' + cellValue[0][1]);
console.log('cellValue [1][0] = ' + cellValue[1][0]);
return cellValue;
}
The structure of a response for a CheckboxGridItem is a 2D array:
Outer array should contain row array
Inner array should contain column elements
The elements themselves should be the column labels
This sample script for a 1 item form submits a 2x2 CheckboxGridItem array choosing "Column 1" for both rows.
Column 1
Column 2
✅
⬜
✅
⬜
function myFunc16() {
const form = FormApp.getActiveForm();
const newRes = form
.getItems()[0]
.asCheckboxGridItem()
.createResponse([['Column 1'], ['Column 1']]);
form.createResponse().withItemResponse(newRes).submit();
}
To submit another response choosing
Column 1
Column 2
⬜
⬜
✅
⬜
the array should be:
[
null, //Row1
['Column 1'] //Row2
]
Similarly, for
Column 1
Column 2
⬜
✅
⬜
⬜
Array:
[
['Column 2'],
null
]
If you insert a array
[
['Column 2']
]
for a 2x2 CheckboxGridItem, the error,
Exception: Wrong number of responses: 1. Expected exactly: 2.
is thrown. It means the array.length doesn't match the number of rows in the grid.
The structure of array can be found by manually submitting the 1 checkbox grid item form and examining the response programmatically using:
//Examine third response [2]
//First item's response [0]
console.log(form.getResponses()[2].getItemResponses()[0].getResponse())

In app scripts, confirm that 2 adjacent cells match, then use a directory tab to push data to a specified sheet ID

Beware, I may be overthinking this.
I keep getting into cyclic thought loops when I'm trying to figure out what to do in this situation, So I will try to explain my thinking and where I am at.
Form is filled out on Google Sheets
Form replies are added to the main Form sheet in the "form responses tab"
Code actives, checking to see if the form was filled correctly (columns A and B match)
if they match, it finds the respective google spreadsheet ID that that row needs to go to, by looking at the directory tab.
That item is then sent over to the appropriate list, which is in it's own sheet
This continues for the rest of the rows of the Main QA Forms Responses tab, until all rows have been checked and there are no more entries.
I've been trying to understand this for hours on end, but might be approaching this all from the wrong angle.
As of right now, this is how far i've gotten in the code:
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var r = s.getActiveRange();
var columnSearchNum = 3;
var columnDatastarts = "C";
var formSheetName = "QA Form Responses";
var directorySheetName = "Program Directory";
var matchingProgramSheetIDColumn = 1;
if(s.getName() == formSheetName && r.getColumn(0) == r.getColumn(1)) {
var sourceRow = r.getRow();
var matchingProgram = sourceRow.getRange(0,0).getValue();
var matchingProgramSheetID = s.getName(Programdirectory)....//[code needed1]
//^^^^ I need a line here to pull matching the data inSheetID column where Matching program's string is
//from this code line, Go to that 'program's sheet'
var programSheet = ss.getSheetByID(matchingProgramSheetID);
var programSheetNumRows = programSheet.getLastRow();
//console.log(programSheetNumRows);
var formSheetNumColumns = s.getLastColumn();
var targetRange = programSheet.getActiveRange()
var targetValue = +s.getRange(columnDatastarts+sourceRow).getValue()
//console.log(targetValue);
var programSheetRange = programSheet.getRange(1,columnSearchNum,programSheetNumRows,1);
//console.log(programSheetRange.getNumRows() +" " +programSheetRange.getNumColumns() + " " + programSheetRange.getValues());
var targetRow = findIndex(programSheetRange.getValues(), targetValue);
//console.log(targetRow);
var target = programSheet.getRange(targetRow, 1);
s.getRange(sourceRow, 2, 1, formSheetNumColumns).moveTo(target);
;
}
}
function findIndex(array, search){
//console.log(array);
if(search == "") return false;
for (var i=0; i<array.length; i++){
//console.log("comparing " + +array[i] + " to "+ +search);
if (+array[i] == +search){
return i+1;
}
}
return -1;
}
You want to find a match for a value on the Form Responses sheet with values in a given column on another sheet, and then return the value in the cell adjacent to the matching cell.
There are probably many ways to do this, but the Javascript method indexOf is an obvious choice if using a script. The following is untested, but the logic is sound.
Insert at [code needed1]
// define the Program sheet
var progsheet = ss.getSheetByName(directorySheetName)
// define the first row of data on the program sheet
var firstRowofData = 3
// get the data for columns one and two of the program sheet.
// starting row=3, starting column=1, number of rows = lastrowminus first row plus 1, number of columns = 2
var progsheetdata = progsheet.getRange(firstRowofData,1,progsheet.getLastRow()-firstRowofData+1,2).getValues()
// get Program manager Column
var ProgManager = progsheetdata.map(function(e){return e[0];})
// search the Program manager Column for the first instance of the matching program value
// indexOf returns index if found, or -1 if not found
var result =ProgManager.indexOf(matchingProgram);
if (result == -1){
// couldn't find a matching program
// do something to quit/error message
return
}
else
{
// the id will be in the row returned by IndexOf and in the adjacent column to the right.
var id = progsheetdata[result][1]
}

Google Script loop overwriting all rows

I've written a script to read data from a website using an API and i'd like to write the output in a google sheet. There are 4 data items per ID of a json object and i'd like to write them to 4 columns, C - F, starting in row 2 until row 32.
I believe your goal as follows.
You want to put the values of [current_price,forecast,demand,available_shares] from the cell "C2" on the active sheet.
For this, how about this answer?
Modification points:
In your script, the value is put to the Spreadsheet with for (i = 2; i < 33; i++) {} every each stockId. By this, the old value is overwritten by the next value. I think that this is the reason of your issue.
I think that in your case, the following flow can be used.
An array is prepared before the for loop.
Put the value of [current_price,forecast,demand,available_shares] to the array.
When the for loop is finished, the array is put to the Spreadsheet.
By this flow, the value of each stockId is put to the array, and then, the array is put to the Spreadsheet. When above points are reflected to your script, it becomes as follows.
Modified script:
function myFunction() {
let values = []; // Added
for (let stockId = 1; stockId < 32; stockId++) {
if (stockId == 24) continue;
var response = UrlFetchApp.fetch("https://api.torn.com/torn/" + stockId + "?selections=stocks&key=" + API);
var content = response.getContentText();
var json = JSON.parse(content);
var current_price = json["stocks"][stockId]["current_price"];
var forecast = json["stocks"][stockId]["forecast"];
var demand = json["stocks"][stockId]["demand"];
var available_shares = json["stocks"][stockId]["available_shares"];
values.push([current_price,forecast,demand,available_shares]); // Added
}
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); // Added
ss.getRange("C2:F" + (values.length + 1)).setValues(values); // Added
}
Reference:
setValues(values)

Sending email when a dropdown item is selected through a Sheet script

I am trying to send an email to the address supplied in column A when the Status drop down in column H has been set to "Completed". Here is what I have so far:
function onOpen() {
sendemail();
}
// This constant is written in column C for rows for which an email
// has been sent successfully.
var EMAIL_SENT = 'EMAIL_SENT';
var COMPLETED = 'Completed';
/**
* Sends non-duplicate emails with data from the current spreadsheet.
*/
function sendemail() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2; // First row of data to process
var numRows = 400; // Number of rows to process
var dataRange = sheet.getRange(startRow, 1, numRows, 3); //grabbing ranges
of values to get
var data = dataRange.getValues(); //getting values
var status = sheet.getRange(startRow, 8, numRows, 1); //grabbing ranges of
//values to get
var data_status = status.getValues(); //getting values
//logic: if a field is populated and both Column C isn't populated, and
//Status is Completed, populate corresponding row in column C and send email.
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var emailAddress = row[0]; // First column
var message = "";
var emailSent = row[2]; // Third column
if (emailSent != EMAIL_SENT && data_status == COMPLETED) { // Prevents
sending duplicates
var subject = 'Sending emails from a Spreadsheet';
MailApp.sendEmail(emailAddress, subject, message);
sheet.getRange(startRow + i, 3).setValue(EMAIL_SENT);
// Make sure the cell is updated right away in case the script is
interrupted
SpreadsheetApp.flush();
}
}
}
This code is heavily influenced from google and I am able to grab all of the info from column H, I'm just unsure of what I am doing wrong. The issue is that it's not working. If I take out the and section of the if statement, it will work just fine, and If I debug the code I can see the array of values given to me from column H. For each "completed" value I need it to send the email, however I don't want the email to be sent if the status is set to completed and the Column C has the value EMAIL_SENT Thank you for your help
I think that your script is almost complete. I think that the script works by modifying one part. So how about this modification?
Modification point :
In your script, data_status is 2 dimensional array. And when the value of each row is compared to "COMPLETED", whole array is comparing.
In order to reflect above to your script, please modify as follows.
From :
if (emailSent != EMAIL_SENT && data_status == COMPLETED) {
To :
if (emailSent != EMAIL_SENT && data_status[i][0] == COMPLETED) {
If this didn't work, please tell me. I would like to modify it.

google appscript form presenting array values for user selection

I'm not sure how to code GAS form buttons to fire a script with dynamic values.
In this scenario, the current sheet cell value is used to Look-Up rows in an adjoining sheet and to populate a result array.
A form then presents a list of buttons containing values from one column of the result array.
Pressing a form button should fire the script postLocationData, and update the current cell and adjoining cells in the row with result array values, and closes the form. At this point, pressing a form button does not seem to do anything. Much thanks in advance for your help :)
function lookUpLocationTest(){
var ui = SpreadsheetApp.getUi();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var cell = sheet.getActiveCell();
var sheetLocations = ss.getSheetByName('LU_Locations');
var arrayRecords = sheetLocations.getRange(2, 3, sheetLocations.getLastRow(), 2).getValues();
var matchingLocations=[];
for (var i=0;i<arrayRecords.length;i++) {
var result = arrayRecords[i][1].indexOf(cell.getValue())
if(result !== -1) {
matchingLocations.push(arrayRecords[i]);
}
}
if(matchingLocations.length === 0){
var result = ui.alert(
'Message:',
'No Matching Location Found.',
ui.ButtonSet.OK);
return 0;
}
Logger.log(' Process - ' + matchingLocations.length + ' Locations have been found.') ; //matchingLocations is a global
// Prep Form HTML with formatted matching Locations
var HTML= '<form><div>'
for(var i=0;i<matchingLocations.length;i++){
HTML += "<div><input type='button' value='" + matchingLocations[i][1]
+ "' onclick='google.script.run.withSuccessHandler(postLocationData).processForm(this.parentNode)'/></div>";
}
var htmlOutput = HtmlService.createHtmlOutput(HTML).setSandboxMode(HtmlService.SandboxMode.IFRAME);
var result = SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Matching Locations');
return 1;
}
function postLocationData(lookUpValue) {
var location = lookUpValuesInArray (matchingLocations, 1, lookUpValue); //matchingLocations is a global
var cell = currCell;
var latLongCol = 3;
cell.setValue(location[0][1]);
cell.getRowIndex();
var sheet = cell.getSheet();
sheet.getRange(cell.getRowIndex(), latLongCol).setValue(location[0][0]);
var temp =1;
}
The function "google.script.run" will be executed on the client side but it will call a function on the serverside (your .gs file). In this case the function you will call is "processForm()" where you are sending "this.parentNode" as parameter.
In you Apps script file (gs file) you should have a function called "processForm()" you didn't post it in the example.
After this function ends, if everything went well, the function "google.script.run" will execute the function that you defined in "withSuccessHandler()". In you example you used "postLocationData".
This function will receive as parameter the results returned from the execution of processForm().
As I mentioned before google.script.run is called on the client side, therefore the function that will be executed if everything went well (the one contained in withSuccessHandler), has to be also in the client side. This means it has to be part of the script contained in the HTML.
As the way you posted the code, I would change the onclick to:
onclick='google.script.run.withSuccessHandler(someJavascriptFunction).postLocationData(this.parentNode)
withSuccessHandler is optional, if you decided to use it, then you should create a html script tag in you HTML variable having that javascript function to show an alert or something that tells the user the result of clicking the button.
You can also create an html file in the appsscript project and call it like: HtmlService.createHtmlOutputFromFile('Index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
This way you can have a cleaner html file and the javascript asociated to it.
Hope this helps.

Categories