Form creating entries *before* entries from script - google-apps-script

I have a sheet which is populated by an expenses form.
Each time the form is submitted, a new row is created in the spreadsheet.
I also have a small script, which I run on the first of the month to add a few lines to the same sheet (some regular expenses that don't need me to use the form).
This all works fine.
The problem is that after I run the script (which adds about 5 lines to the sheet), when I go back the form, and submit it. New entries are being put ahead of the scripts ones.
So:
1) Submit expenses form -> new entry in row 2.
2) submit expenses form -> new entry in row 3.
3) run script -> new entries in rows 4,5,6,7,8,9
4) submit expenses form -> new entry in row 4. Rows 4,5,6,7,8,9 shift down one
Can anyone advise?

Why is this happening ?
Actually, I think google form internally keeps a track of last row number where it have inserted your response. So that next time it can decide where to put your new response.
So when you're manually[by script] entering data in sheet and not through google form so the counter of google form row number is not updated and that's unaware of the fact that you have inserted some data, so it tried to insert data at the same position that it remembers.
Hope this will help you, at least to some extend.
How to solve this ?
Solution to your situation would be, as mentioned in another answer; you can use the FormService to submit your data as "form responses" and thus the google form internal counter also increases it's count. Everything is in sync.

Since your script is not doing any manipulations to the data, but simply adding recurring values to the form, use the Forms Service to submit your data to the form directly as responses. With this approach, the expenses will all live within the Form responses in case you ever disconnect the form from the sheet.
function addRecurringExpenses() {
var form = FormApp.openById("FORM_ID");
// Create an array of objects where each object represents one of your recurring expenses.
// The key-value pairs correspond to the questions in the fom
var recurringExpenses = [{"question1": "1-response1", "question2": "1-response2", "question3": "1-response3"},
{"question1": "2-response1", "question2": "2-response2", "question3": "2-response3"},
{"question1": "3-response1", "question2": "3-response2", "question3": "3-response3"}]
var questions = form.getItems(); // Get all of the questions in the form
for (var i=0; i<recurringExpenses.length; i++) { // Loop through the recurring expense
var response = form.createResponse(); // Create a new response that will house all of the answers for this one expense
var question1 = questions[0].asTextItem(); // Not all questions are "TextItems" – https://developers.google.com/apps-script/reference/forms/
var answer1 = question1.createResponse(recurringExpenses[i]["question1"]);
var question2 = questions[1].asTextItem();
var answer2 = question2.createResponse(recurringExpenses[i]["question2"]);
var question3 = questions[2].asTextItem();
var answer3 = question3.createResponse(recurringExpenses[i]["question3"]);
// Add each answer to the response object
response.withItemResponse(answer1).withItemResponse(answer2).withItemResponse(answer3);
response.submit(); // Submit the response to the form
}
}

Related

Google Sheets: Action Based on Birthday

I'm trying to send myself either an email or copy the row to a new sheet when it's someone's birthday or hire date anniversary. Copying the line to a new sheet would allow me to use zapier to notify me of the update. Either would work. The sheet uses a form to collect data.
I've built a few scripts but nothing that had to do with dates. I'm just struggling with this one and have tried a few examples I could find with no luck.
Here is this sheet. It's view only so just let me know if you need more access.
I understand that you want to replicate your form responses Sheet in another Sheet (let's call it Zapier Sheet) automatically each time that a new form response is added. You can achieve that goal developing an Apps Script code that runs at each form response. In that case you can use a code similar to this one:
function so62400514() {
var formSheet = SpreadsheetApp.openById(
'{FORM SHEET ID}').getSheets()[0];
var zapierSheet = SpreadsheetApp.openById(
'{ZAPIER SHEET ID}').getSheets()[0];
var formData = formSheet.getRange(1, 1, formSheet.getLastRow(), formSheet
.getLastColumn()).getValues();
var zapierData = zapierSheet.getRange(1, 1, zapierSheet.getLastRow(),
formSheet.getLastColumn()).getValues();
var recorded = false;
for (var fr = 0; fr < formData.length; fr++) {
for (var zr = 0; zr < zapierData.length; zr++) {
if (formData[fr].toLocaleString() == zapierData[zr].toLocaleString()) {
recorded = true;
}
}
if (recorded == false) {
zapierSheet.appendRow(formData[fr]);
} else {
recorded = false;
}
}
}
This code will first open both sheets (using SpreadsheetApp.openById() and Spreadsheet.getSheets()) to select the data with Sheet.getRange (setting boundaries with Sheet.getLastRow() and Sheet.getLastColumn()) and reading it using Range.getValues(). After that operation the data will get iterated using the property Array.length as the perimeter. The iteration compares each row from the form Sheet to every row of the zapier sheet (to accomplish that, I first parsed the row as a string with Date.toLocaleString()). If the form row is found in the zapier sheet, the boolean recorded will flag to true. After every row on the zapier sheet gets compared to the form row, the code will write it down on the zapier sheet based on the boolean flag.
As explained in the previous paragraph, this code will take the form sheet rows not present in the zapier sheet; and paste them on the zapier sheet. I used this approach to prevent missing any row (as it could happen when simultaneous users answer the form all at once). To make this fire automatically you'll need to set up an installable trigger with these settings:
As an example, let's say that we have these form responses:
And our initial sample zapier sheet looks like this one below. Please, notice how several past rows are missing;
After running the script (as it will do automatically) this would be the result:
I suggest running the script manually for an initial setup. If the timestamps diverge, please check if both spreadsheets share time zones. Don't hesitate to ask me further questions to clarify my answer.

Google Apps - on Form Submit - what is the name of the sheet

The following code from Alex's code on Google Forms onsubmit
will send an email when a form attached to the spreadsheet is submitted.
function onSpreadsheetSubmit(e) {
var row = e.range.getRow();
MailApp.sendEmail("me#example.com",
"Your subject, rows: "+ row,
"A new application has been submitted on row: "+
row,
{name:"From your friendly spreadsheet"});
}
The code successfully returns the row number of the submission using e.range.getRow()
How can I get the name of the sheet that the form is connected to?
E.g. something like e.range.getSheet()
Although this question How to get form values in the submit event handler? discusses retrieving the event values, I do not believe that it addresses directly the retrieval of the name of the sheet.
And how can I elegantly get the data of the new row?
If all you're trying to do is return the sheet name from your event object, use:
var name = e.range.getSheet().getName();
To access the values submitted to the form, you can use:
var values = e.values;
This returns the submitted values in an array.
There are a lot of possibilities with event objects, you should really look into the documentation to find out how to use them to your advantage.
References:
Event Objects documentation.

Populate a Google Form responses from a Google Spreadsheet

I am trying (and not succeeding at the moment) to populate a Google Form from a Google Spreadsheet using the items that I have picked up from this website and this extremely useful answer provided by Mogsdad here.
Ideally, I'm looking for:
The Logger.log (URL) logs URL's for all the data stored in the spreadsheet, is it possible to just log the last entry and use this to generate the URL?
Is it then possible for the pre-filled URL to auto submit once populated with the data? I have found this useful article here which suggests that this can be done?
The data that is stored in the Google Spreadsheet is data captured from another Google Form. This is due to the need of using Excel (lack of Internet connectivity) with a concatenate formula to merge all cells with data into one. This is then submitted on the other Google Form which has this script to split the data out by column ready to answer the questions with. Will this impact the trigger needed to auto submit when a submission is made?
May I add that I have a rather limited understanding on this so please go easy if this seems rather easy to do!
is it possible to just log the last entry and use this to generate the URL?
I'm not sure I follow you here - in that other answer, the Logger.log() statement was just to demonstrate that you could generate the correct URL, if you wanted to distribute it. Instead of logging it, you'd just use the content of the variable url.
But let's move on, because I think this is a little off your path.
Is it then possible for the pre-filled URL to auto submit once populated with the data?
There's a better starting point. Can we programmatically submit information from a spreadsheet into a google form? Sure! See Use App Scripts to open form and make a selection. It's a more reliable way to do the job than what you see in the "URL Tricks" post.
In the case of "auto submit", or simulating a form submission, you don't need to worry about the pre-filled URL at all. That's a shortcut for pesky humans. What you want is to put together the payload for a POST request, instead, and have a computer bypass the form UI altogether.
Something about Excel... Will this impact the trigger needed to auto submit when a submission is made?
(Sounds like...) You are using Form1 to get data into Spreadsheet1, then expecting to react to the (human?) submission of Form1 by having the machine submit Form2 after breaking apart the data from Form1.
Yeah, you'll need to be careful that the column split is done before you try to read the information to submit Form2.
I suggest that you would be best served with a form-submission trigger function for Spreadsheet1 that splits the string received from Form1 then immediately sends the POST to Form2. I'd then record the fact that this action has occurred, using the technique from Spreadsheet Email Trigger.
Is it then possible for the pre-filled URL to auto submit once populated with the data? I have found this useful article here which suggests that this can be done?
As Mogsdad said
There's a better starting point. Can we programmatically submit information from a spreadsheet into a google form? Sure!
According to an edit to Use App Scripts to open form and make a selection, the payload/post method is now obsolete. The alternative is to use the Form App service. Below is an adaptation that I made to evenBetterBuilURLs1 by Mogsdad to submit responses instead of create pre-filled URLs and to be used in a stand-alone Google Script Project File.
The original code lines that were changed are commented out. Also I inserted some breaklines to avoid the horizontal scroll bar.
/**
* Use Form API to populate form
*
* Addapted from https://stackoverflow.com/a/26395487/1677912
*/
function populateFormResponses() {
//var ss = SpreadsheetApp.getActive();
var id = '11KDxp1C6jAZaTMNlGHke8zEzQ7aZrFSFGABdwUHEV80';
var ss = SpreadsheetApp.openById(id);
var sheet = ss.getSheetByName("Form Responses 1");
var data = ss.getDataRange().getValues(); // Data for pre-fill
var headers = data[0]; // Sheet headers == form titles (questions)
var formUrl = ss.getFormUrl(); // Use form attached to sheet
var form = FormApp.openByUrl(formUrl);
var items = form.getItems();
//var urlCol = headers.indexOf("Prefilled URL"); // If there is a column labeled this
// way, we'll update it
// Skip headers, then build URLs for each row in Sheet1.
for (var row = 1; row < data.length; row++ ) {
//Logger.log("Generating pre-filled URL from spreadsheet for row="+row);
Logger.log("Generating response from spreadsheet for row="+row);
// build a response from spreadsheet info.
var response = form.createResponse();
for (var i=0; i<items.length; i++) {
var ques = items[i].getTitle(); // Get text of question for item
var quesCol = headers.indexOf(ques); // Get col index that contains this
// question
var resp = ques ? data[row][quesCol] : "";
var type = items[i].getType().toString();
Logger.log("Question='"+ques+"', resp='"+resp+"' type:"+type);
// Need to treat every type of answer as its specific type.
switch (items[i].getType()) {
case FormApp.ItemType.TEXT:
var item = items[i].asTextItem();
break;
case FormApp.ItemType.PARAGRAPH_TEXT:
item = items[i].asParagraphTextItem();
break;
case FormApp.ItemType.LIST:
item = items[i].asListItem();
break;
case FormApp.ItemType.MULTIPLE_CHOICE:
item = items[i].asMultipleChoiceItem();
break;
case FormApp.ItemType.CHECKBOX:
item = items[i].asCheckboxItem();
// In a form submission event, resp is an array, containing CSV strings. Join
// into 1 string.
// In spreadsheet, just CSV string. Convert to array of separate choices, ready
// for createResponse().
if (typeof resp !== 'string')
resp = resp.join(','); // Convert array to CSV
resp = resp.split(/ *, */); // Convert CSV to array
break;
case FormApp.ItemType.DATE:
item = items[i].asDateItem();
resp = new Date( resp );
break;
case FormApp.ItemType.DATETIME:
item = items[i].asDateTimeItem();
resp = new Date( resp );
break;
default:
item = null; // Not handling DURATION, GRID, IMAGE, PAGE_BREAK, SCALE,
// SECTION_HEADER, TIME
break;
}
// Add this answer to our pre-filled URL
if (item) {
var respItem = item.createResponse(resp);
response.withItemResponse(respItem);
}
// else if we have any other type of response, we'll skip it
else Logger.log("Skipping i="+i+", question="+ques+" type:"+type);
}
// Submit response
response.submit();
// Generate the pre-filled URL for this row
//var editResponseUrl = response.toPrefilledUrl();
// If there is a "Prefilled URL" column, update it
//if (urlCol >= 0) {
// var urlRange = sheet.getRange(row+1,urlCol+1).setValue(editResponseUrl);
//}
}
};

Accessing user entered data upon submit in google forms

I have a google-form that has the following two fields:
Email address: - A text box
Tool: - A radio button
Tool 1
Tool 2
Tool 3
The user would enter his email address and select a tool and click submit. I would like the following message to appear:
Thanks for responding. An email has been sent to you to at entered email address to download selected tool.
I have the following piece of code in the script editor
function emailFormSubmission() {
var form = FormApp.getActiveForm();//the current form
var dest_id = form.getDestinationId(); //the destination spreadsheet where form responses are stored
var ss = SpreadsheetApp.openById(dest_id);//open that spreadsheet
var theFormSheet = ss.getSheets()[0]; //read the first sheet in that spreadsheet
var row = theFormSheet.getLastRow(); //get the last row
var emailid = theFormSheet.getRange(row,2,1,1).getValue();//get column 2 corresponding to the email id. column 1 is timestamp. so, skip that.
var tool = theFormSheet.getRange(row,3,1,1).getValue();//get column 3 corresponding to the selected tool.
form.setConfirmationMessage('Thanks for responding. An email has been sent to you '+ emailid + ' to download' + tool);
}
I have also set the triggers to be Run -> emailFormSubmission, Events -> from Form , onFormSubmit.
What happens is: Suppose the first user ('A') enters his information and clicks submit. His entered information gets displayed correctly. When second user ('B') enters his information and clicks submit, A's information is displayed. When third user ('C') enters his information and clicks submit, then B's information is displayed. I found that the issue is with "getlastrow()" since the spreadsheet is updated after emailFormSubmission is processed.
Whats wrong with the above code? How do I fix this?
UPDATE
Based on #wchiquito's comments, I changed the code to following to make it work.
function emailFormSubmission(e) {
var form = FormApp.getActiveForm();
//Check this link on how to access form response:
//https://developers.google.com/apps-script/understanding_events?hl=en
var responses = e.response;//e is of type formresponse.
var emailid = responses.getItemResponses()[0].getResponse();
var tool = responses.getItemResponses()[1].getResponse();
Logger.log(emailid);
Logger.log(tool);
form.setConfirmationMessage('Thanks for responding. An email has been sent to '+ emailid + ' with instructions to download ' + tool +'. If you do not find our email in your inbox, please check your spam folder');
Logger.log(form.getConfirmationMessage());
}
Remember that the event On form submit (Understanding Events) receives a parameter that has the following structure:
values​​
range
namedValues​​
and you can do something like:
function emailFormSubmission(e) {
...
var row = e.range.getRow();
...
}
Try the following code to observe the structure of the parameter e:
function emailFormSubmission(e) {
...
Logger.log(e);
...
}
UPDATE
First, excuse my confusion, I showed you the structure of a Spreadsheet form submit event when you really are using a Form submit event.
Sure enough, a Form submit event has the following structure:
response
Returning an object of type FormResponse.
Therefore, defining the event: On submit form (Form submit event), you can do something like the following:
function emailFormSubmission(e) {
var itemResponses = e.response.getItemResponses();
for (var i = 0, len = itemResponses.length; i < len; ++i) {
Logger.log('Response #%s to the question "%s" was "%s"',
(i + 1).toString(),
itemResponses[i].getItem().getTitle(),
itemResponses[i].getResponse());
}
}
However, the confirmation message set according to the data sent as responses of the form, does not seem very clear, you can set the message, but will not display for the active response, if not for the next.
My first guess is these two lines right here:
var emailid = theFormSheet.getRange(row,2,1,1).getValue();//get column 2 corresponding to the email id. column 1 is timestamp. so, skip that.
var tool = theFormSheet.getRange(row,3,1,1).getValue();//get column 3 corresponding to the selected tool.
When you call getLastRow() on a sheet - you're getting the last row. Sure, but considering the order of events and how these values are processed, you need a +1, to get the most recent submission. Currently you're one row behind when your code runs to update the Form confirmation message.
So just change your code to the following:
var emailid = theFormSheet.getRange(row+1,2,1,1).getValue();
var tool = theFormSheet.getRange(row+1,3,1,1).getValue();
Spreadsheets are the most confusing of Google services, in my opinion. When you get values in the Spreadsheet, they're returned as an [] or [][] depending on what your Range is when you call getValues(). But getRange() on a sheet starts at index 1 (to make it easier to read in code I suppose). Often times I find that I have an off-by-one error because of the way data is passed around. Just keep that in mind as you work with Spreadsheets :)
Short answer: want you want can't be done with Google forms.
Explanation:
form.setConfirmationMessage() sets the confirmation message for the form as stored on the server, not for the current active form. Same applies for example for form.setTitle(). The active form will not be modified. One would expect different behaviour for the confirmation message, but alas, this is not the case.
Yes, you can do this with the add-on "Formfacade".
It's free to use in 1 form.

Allowing others to add Google tasks

I am looking for a way for employees to send me an email or add information to a spreadsheet that will then add tasks to my task list. Ideally, the script would capture the task list, task, due date, and any notes.
I have already successfully implemented five scripts (five task lists) that allow my employees to add tasks to specific tasklists, following this script shown below. This works OK but does not have the capacity to add due dates or notes:
Automated email to task list API
I recently came across references to scripts that monitors task lists, and then posts them to a spread sheet, including task, due dates, notes, etc. It strikes me that a spreadsheet might be a better way to do this though it does not have the convenience of email:
Task list to spreadsheet API
I wonder if the REVERSE can be done. I envision a spreadsheet that I could give my employees access to, with two worksheets (NEW and PROCESSED) with columns:
TASKLIST TASK DUE DATE NOTES
and the script would run through this every hour or two. Anything in NEW would be processed and added to my task list, then moved to the end of PROCESSED.
Does anyone know of something like that out there? Alternatively, perhaps there are ways to change the email script so that it moves anything in the body of the email into the NOTES section of the task. I am a raw newbie at this BTW. Thanks.
you should replace
var newTask = Tasks.newTask().setTitle(title);
by
var newTask = Tasks.newTask().setTitle(title).setDue(date).setNotes(notes);
I'm also stuck in the way
I can from a spreadsheet :
- Create a new tasklist
- Create a new task in a dedicated tasklist (with due date and notes)
I can from the Gtasks :
- Check if the task is completed and mark it as completed in the spreadsheet
- Check if the task still exists in the spreadsheet and remove it if necessary
I'm still looking for a way to make a task completed in GTasks when it's closed in spreadsheet
All the functionality exists for you to accomplish this, but I don't know if there is a pre-built script out there that does what you want. You may want to look into use a Google Form that saves data to the spreadsheet, and then create a trigger for form submit that scoops up the data and creates a new task using it.
Is this [part] of what you're looking for?
https://developers.google.com/apps-script/articles/google_apis_reading_list
It syncs a Spreadsheet based task list with a your regular Task List, and if you mark the task done in gmail, it records that back in the spreadsheet.
// Fetch the list of URLs to keep synchronized
var articleUrls = SpreadsheetApp.getActiveSheet().getRange("A2:A");
for (var rowNum = 0; rowNum < articleUrls.getNumRows(); rowNum++) {
// Limit our range to a single cell containing a URL
var oneUrlCell = articleUrls.offset(rowNum, 0, 1, 1);
if (oneUrlCell.getComment() === "") {
// This is a new URL that needs to be shortened/inserted
var urlText = oneUrlCell.getValue();
if (urlText !== "") {
// Shorten the URL
Logger.log("Adding task for url: " + urlText);
var toShorten = UrlShortener.newUrl().setLongUrl(urlText);
var shortened = UrlShortener.Url.insert(toShorten);
// Insert the shortened URL into our reading list
var taskToInsert = Tasks.newTask().setTitle(shortened.getId());
taskToInsert.setNotes(urlText);
var newTask = Tasks.Tasks.insert(taskToInsert, readingListId);
// Save the new ID as our comment.
oneUrlCell.setComment(newTask.getId());
}
} else {
// This URL has already been inserted, update the status
var existingTask = Tasks.Tasks.get(readingListId, oneUrlCell.getComment());
if (existingTask.getStatus() === "completed") {
var absRowNum = oneUrlCell.getRow();
var completedCell = sheet.getRange(absRowNum, 2);
completedCell.setValue("Yes");
}
}
Should be part of the solution, no?
I'm looking to make something a bit bigger myself.