Sending data to Google Spreadsheet via Ajax - javascript

I'm having problems sending data to a Google Spreadsheet via Ajax.
I followed this guide to achieve this: https://medium.com/#dmccoy/how-to-submit-an-html-form-to-google-sheets-without-google-forms-b833952cc175
My own code looks as simple as this:
var data = {"user": "bar"};
var url = 'https://script.google.com/macros/s/AKfycby6BzLifopR36ZBJ2WqDMNsndCYUpHihSOfhomfSRZXcdetvOA/exec';
$('.my-button').on('click', function (e) {
$.ajax({
url: url,
method: "GET",
dataType: "json",
data: data
}).done(
function (__e) {
console.log('Remote sheet updated!', __e);
}
).fail(function (__e) {
console.log('Remote sheet update failed', __e);
});
});
This is what my spreadsheet looks like:
This is the code from the tutorial that goes into the Google scripteditor.
function doGet(e){
return handleResponse(e);
}
// Enter sheet name where data is to be written below
var SHEET_NAME = "DATA";
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
function handleResponse(e) {
// shortly after my original solution Google announced the LockService[1]
// this prevents concurrent access overwritting data
// [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
// we want a public lock, one that locks for all invocations
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("key", doc.getId());
}
In the code provided by the author of the tutorial all I did is change the sheet name in line 9 to "DATA"
// Enter sheet name where data is to be written below
var SHEET_NAME = "DATA";
...which is the name of the sheet.
The problem is now, when I click "my-button", it gets send successfully, but returns an error object. The error doesn't contain any additional information though. It seems to go into the "catch" section of the Google script, but doesn't inform me of what exactly went wrong. Now I don't even know how to debug this, let alone understand, what went wrong.
I already tried all kinds of weird things like using JSON.stringify, none of that helped. It should work like this anyway, so I have no idea what's the problem here.

Related

How to Auto-Run a Script in Google Sheets when someone's Data Entry to the Sheet?

I want to Auto-Run Script in Google Sheets every time someone registers from my custom form on my website and the Data Entry to the Sheet.
I tried with adding an OnSubmit event on Trigger but it did not work because I don't use Google Forms I use a Script to collect the data from my website.
Here is the Send Emails Script that I want to Auto-Run and send an email each time someone's data comes into the sheet:
function sendMail() {
const emailTemp = HtmlService.createTemplateFromFile("email");
const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("form1");
const startRow = 2;
const data = sh.getRange(startRow, 2, sh.getLastRow() - 1, sh.getLastColumn() - 1).getValues();
data.forEach((row, i) => {
emailTemp.first = row[2];
emailTemp.last = row[3];
emailTemp.phone = row[9];
let htmlMessage = emailTemp.evaluate().getContent();
let emailSent = row[11];
if (emailSent != "EMAIL_SENT") {
GmailApp.sendEmail(
row[1],
"Thank you!",
"Your email doesn't support HTML.",
{ name: "Email App", htmlBody: htmlMessage });
sh.getRange(startRow + i, 13).setValue("EMAIL_SENT");
}
});
}
Here is the Data Collection Script on my website:
// Variable to hold request
var request;
var url = window.location.href;
$( document ).ready(function() {
$('#url').val(url);
});
console.log(url);
// Bind to the submit event of our form
$("#request-form").submit(function(event){
// Abort any pending request
if (request) {
request.abort();
}
// setup some local variables
var $form = $(this);
// Let's select and cache all the fields
var $inputs = $form.find("input, select, button, textarea");
// Serialize the data in the form
var serializedData = $form.serialize();
// Let's disable the inputs for the duration of the Ajax request.
// Note: we disable elements AFTER the form data has been serialized.
// Disabled form elements will not be serialized.
$inputs.prop("disabled", true);
// Fire off the request to /form.php
request = $.ajax({
url: "https://script.google.com/macros/s/AKfycbwa50RJFxZfnBXQX-LBHR4OD6Lzxb0a2tAclBzGV57nT0XY3WU/exec",
type: "post",
data: serializedData
});
// Callback handler that will be called on success
request.done(function (response, textStatus, jqXHR){
// Log a message to the console
console.log("Hooray, it worked!");
console.log(response);
console.log(textStatus);
console.log(jqXHR);
});
// Callback handler that will be called on failure
request.fail(function (jqXHR, textStatus, errorThrown){
// Log the error to the console
console.error(
"The following error occurred: "+
textStatus, errorThrown
);
});
// Callback handler that will be called regardless
// if the request failed or succeeded
request.always(function () {
// Reenable the inputs
$inputs.prop("disabled", false);
console.log("It's running");
window.location.href = 'success.html';
});
// Prevent default posting of form
event.preventDefault();
});
And here is the Script I have used on the Sheet and Deploy it As Web App, and made it accessable to Anyone, even anonymous.
// 1. Enter sheet name where data is to be written below
var SHEET_NAME = "Leads";
// 2. Run > setup
//
// 3. Publish > Deploy as web app
// - enter Project Version name and click 'Save New Version'
// - set security level and enable service (most likely execute as 'me' and access 'anyone, even anonymously)
//
// 4. Copy the 'Current web app URL' and post this in your form/script action
//
// 5. Insert column names on your destination sheet matching the parameter names of the data you are passing in (exactly matching case)
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
// If you don't want to expose either GET or POST methods you can comment out the appropriate function
function doGet(e){
return handleResponse(e);
}
function doPost(e){
return handleResponse(e);
}
function handleResponse(e) {
// shortly after my original solution Google announced the LockService[1]
// this prevents concurrent access overwritting data
// [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
// we want a public lock, one that locks for all invocations
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date(), "UTC-7");
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("key", doc.getId());
}
Thank you.
If you are sending the data to the web app via POST request from the client, then use the doPost(e) function in your Apps Script to do what you want.
Include the sendMail() function inside your doPost(e) function before it returns.
Example:
before you execute this line
return ContentService.createTextOutput(JSON.stringify({"result":"success", "row": nextRow})).setMimeType(ContentService.MimeType.JSON);
in handleResponse(e) I would have the program execute sendMail(). I would also have 2 different functions for doGet and doPost but that's up to you.

Formatting Parameter for Google Scripts

I am building out a script on my Google Sheet that will catch a Webhook POST from my CRM and update a row on the worksheet. Everything is working perfectly, except I can't figure out how to format this one parameter.
It comes over in the webhook payload like this:
...
"Dinner Seminar Session Choice":"Thursday, Feb 27th at 5:30pm",
...
The Google Sheet Script is this:
//this is a function that fires when the webapp receives a GET request
function doGet(e) {
return HtmlService.createHtmlOutput("request received 11:40");
}
//this is a function that fires when the webapp receives a POST request
function doPost(e) {
var params = JSON.stringify(e.postData.contents);
params = JSON.parse(params);
var myData = JSON.parse(e.postData.contents);
var FirstName = myData.first_name;
var LastName = myData.last_name;
var Phone = myData.phone;
var Session = myData.DinnerSeminarSessionChoice;
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = Math.max(sheet.getLastRow(),1);
sheet.insertRowAfter(lastRow);
var timestamp = new Date();
sheet.getRange(lastRow + 1, 1).setValue(timestamp);
sheet.getRange(lastRow + 1, 2).setValue(FirstName);
sheet.getRange(lastRow + 1, 3).setValue(LastName);
sheet.getRange(lastRow + 1, 4).setValue(Phone);
sheet.getRange(lastRow + 1, 5).setValue(Session);
sheet.getRange(lastRow + 1, 6).setValue(params);
SpreadsheetApp.flush();
return HtmlService.createHtmlOutput("post request received");
}
function myFunction() {
}
You can see on line 14, I'm trying to set the content of that param as var Session. But it doesn't seem to work with or without the spaces.
I'm sure this is a simple formatting error, but I just don't know how to do it. I appreciate any help.
Jason
As pointed to by #TheMaster's comments.
Property accessors can be used with "[ ]" to read or write property names with spaces.
Example:
const json_response = {
"oldValue": "false",
"trigger Uid": "30023847",
"user": {
"nick name": "fat.mike",
}
}
console.log(json_response.oldValue)
json_response.oldValue = null;
console.log(json_response['oldValue'])
console.log(json_response['trigger Uid'])
console.log(json_response.user);
console.log(json_response.user['nick name'])

How to do a JSON api call with error response?

I use Javascript to retrieve through a JSON api call the amount of active products pasted in a certain filter.
My typicall response would be
{"total":34,"product_ids":["PRODUCT1","PRODUCT2",....."]}
My script is working fine when products are present but when none of the products are active the response will be:
{"error":"No products found, please check request settings"}
In this case the script will crash.
What I tried to do is to set the var NumEdPicks to 0 when I get an error but I don't really know how as the script is crashing when it doesn't find "total".
This is what the retrieve part of the script looks like
// Retrieve
var url = 'http://api.jetlore.com/products/products_by_filter.json?jl_cid=' + clientID + '&filter=' + filterName + '&per_page=' + maxCount + '&page=1';
var response = HTTP.Get(url);
var responseObj = Platform.Function.ParseJSON(response["Content"]);
var NumEditorsPick = responseObj.total;
if(NumEditorsPick>maxCount){ var NumEditorsPick = maxCount;}
I would like to set NumEditorsPick to 0 when I get the error response.
Some things I was thinking about but which isn't working:
var NumEditorsPick = responseObj.total || 0
or
var NumEditorsPick = ‘total’ in responseObj ? responseObj.total : 0
How to define NumEditorsPick when there is no total?
I've tried so far:
if (responseObj.hasOwnProperty('total')){
var NumEditorsPick = responseObj.total;
}else{
var NumEditorsPick = 0;
}
And
if (responseObj.has("total")){var NumEditorsPick = responseObj.total;
}
if (responseObj.has("error")){var NumEditorsPick = 0;
}
Both are crashing the execution of my script, so I'm starting to think that when there is an error response it just stops the script and ignores the rest, would that be possible? In that case, how to ignore this error response?
EDIT:
After using the try/catch method as suggested in the comments, I managed to finally make it work:
var NumEditorsPick;
try {
var response = HTTP.Get(url);
var responseObj = Platform.Function.ParseJSON(response["Content"]);
NumEditorsPick = responseObj.total;
} catch (error) {
NumEditorsPick = 0;
}
You can use Javascript's hasOwnProperty() to check if the parse JSON has the key you're looking for.
In this case, I'd be something like:
var responseObj = Platform.Function.ParseJSON(response["Content"]);
if (responseObj.hasOwnProperty('error')){
// handle error msg
}else{
// do something else
}
Here's a simple example using the JSON input you've provided.
Update
Ok, so my initial answer was based on what you said here:
My script is working fine when products are present but when none of
the products are active the response will be:
{"error":"No products found, please check request settings"}
But the service you're calling does not return a JSON string containing the error. Instead it returns a 404 and therefore, any attempt to parse or use the response content is not valid.
So, to start, you could try wrapping your HTTP.Get(url)in a try/catch method and on the catch clause set the NumEdPicks to zero.
Another option would be to check HTTP.Get() method documentation to see if the response object has a status (e.g: response.Status) or if you can pass a callback function for response and error, like this example in AJAX:
$.ajax({
url: 'yourUrl',
type: 'GET',
success: function(data){
// Set NumEdPicks to total and do other stuff
},
error: function(data) {
// Set NumEdPicks to zero
}
});

Post data to google spreedsheet using php

I am trying to post data using curl php but the issue I am facing the data saved in Google sheet is always coming undefined. I have used below code which is attached with description and also attached the image url which is showing the value coming from is php script I am using php and curl to send data and trying to savethat data in google sheet.
Image showing undefined value: https://ibb.co/0n5WKdz
PHP SCRIPT
function send_to_url($url, $data)
{
$ch = curl_init($url);
//Set a POST method
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
//curl_setopt($ch, CURLOPT_TIMEOUT, 6);
//Set data to JSON application/json
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$jsonDataEncoded = json_encode($data);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonDataEncoded);
//POST to URL
$result = curl_exec($ch);
//Get result from POST if any
$result_post = urldecode(json_encode($result));
curl_getinfo($ch);
curl_close($ch);
return $result;
}
$users =
array("Date"=>"Timestamp","FirstName"=>"John","LastName"=>"Cena","Email"=>"john78#example.com");
send_to_url("GOOGLE_APP_SCRIPT_URL",$users);
GOOGLE SCRIPT
var SHEET_NAME = "DATA";
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
// If you don't want to expose either GET or POST methods you can comment out the appropriate function
function doGet(e){
return handleResponse(e);
}
function doPost(e){
return handleResponse(e);
}
function handleResponse(e) {
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": headRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("key", doc.getId());
}
Please check my code and provide the solution and let me know where I am making mistake.
In your scripts, I think that your php script works and Google Apps Script is required to be modified a little. So how about this modification?
Modification points:
When a JSON object ({"key1": "value1", "key2": "value2"}) is sent to Web Apps as POST request, e of doPost(e) becomes as follows.
{
"parameter": {},
"contextPath": "",
"contentLength": 36,
"queryString": "",
"parameters": {},
"postData": {
"type": "application/json",
"length": 36,
"contents": "{\"key1\": \"value1\", \"key2\": \"value2\"}",
"name": "postData"
}
}
By this, when you want to retrieve the values from the object, the object is required to be parsed like var postData = JSON.parse(e.postData.contents). It seems that although the data is sent as application/json, the object is not automatically parsed.
About the line of if (headers[i] == "Timestamp"){, your sample image doesn't have Timestamp. I thought that it might be Date.
Modified script:
Please modify as follows.
From:
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
To:
var postData = JSON.parse(e.postData.contents);
var row = headers.map(function(e) {return e == "Date" ? new Date() : postData[e]});
Result:
When your php script is run, the values are put to the Spreadsheet as follows.
Note:
When you modified Google Apps Script of Web Apps, please redeploy the Web Apps as new version. By this, the latest script is reflected to the Web Apps.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
map()
If I misunderstood your question and this was not the result you want, I apologize.

Debugging multiple Google form submissions on single click?

I'm trying to do a kind of purchase request app built off of a Google Spreadsheet. For awhile, (like the whole time I've been working on this), my code was working. For each line in the order sheet, it would loop through the values, fill in the Google form inputs, submit the form, then start the process again.
Yesterday I noticed it was submitting each line twice submitting the first line once, second line twice, third line three times, and so on. Then it stopped submitting at all. Then it started again submitting multiple times, then stopped. Could you guys please take a look at my code and tell me what I'm doing wrong?
function formSubmit() {
//Create unique ID (number of milliseconds since 1/1/70)
var d = new Date();
var n = d.getTime();
var uniqueID = n.toString();
//Loop through the lines of the order, fill in the values, submit
$('.orderline').each(function(i, obj) {
//Stop the default redirect so we can submit multiple times
$('#ss-form').submit(function(e) {
e.preventDefault();
$.ajax({
url: "https://docs.google.com/a/vt.edu/forms/d/e/1FAIpQLSf77MuDLeqyPbuDCBcpVagi6-hdiUpgZtr0CbuJ3kO-vXPswg/formResponse",
data: $(this).serialize(),
type: "POST",
dataType: "jsonp",
success: function(data) {
console.log("Submission successful");
},
error: function(xhr, status, error) {
console.log("Submission failed: " + error);
},
});
});
$("#entry_1725077600").val(uniqueID);
var name = $("#personname").val();
var email = $("#personemail").val();
$("#entry_1352722479").val(name);
$("#entry_1024015951").val(email);
//etc.
$("#ss-form").submit();
});
The form is public if you guys want to take a look. Note I have two forms submitting at once on the same click; the one above is for the items in the order, the second one is for metadata about the order.
EDIT: formSubmit() is being called from a second function that uploads files to Google Drive (if there's a better way to do this please do let me know):
if(document.getElementById('fUpload').value!='') {
var user = gapi.auth2.getAuthInstance().currentUser.get();
var oauthToken = user.getAuthResponse().access_token;
var uploadObj = $("[id$=fUpload]");
var file = uploadObj.prop("files")[0];
var metadata = {
'title': file.name,
'description': " ",
'mimeType': file.type || 'application/octet-stream',
"parents": [{
"kind": "drive#file",
"id": "0B5zM5ktmwJ2fN0c3RWYxWC1rUzQ"
}]
};
var arrayBufferView = new Uint8Array(file);
var uploadData = new Blob(arrayBufferView, {type: file.type || 'application/octet-stream'});
try{
var uploader =new MediaUploader({
file: file,
token: oauthToken,
metadata: metadata,
params: {
convert:false,
ocr: false
}
});
uploader.upload();
} catch(exc){
showErrorMessage("Error: " + exc);
$("#fUpload").val(" ");
}
} else {
formSubmit();
}
});
And then for successful responses:
MediaUploader.prototype.onContentUploadSuccess_ = function (e) {
if (e.target.status == 200 || e.target.status == 201) {
var response = e.target.response; //Get the response body
var parsed = JSON.parse(response); //Parse the response body to JS object
var fileID = parsed.id; //Get the file ID from the response
var linkToFile = "https://drive.google.com/open?id=" + fileID;
$("#entry_1703377267").val(linkToFile); //Add the file ID as the value of the file ID input field
formSubmit(); //Run the rest of the form submit functions
this.onComplete(e.target.response);
} else if (e.target.status == 308) {
this.extractRange_(e.target);
this.retryHandler.reset();
this.sendFile_();
}
};
EDIT 2: I never see success or error messages in the console. Also, it looks like things aren't being submitted twice, they're being submitted in a pattern: first item once, second item twice, third item three times, etc.

Categories