Reject Google Forms submission or remove response - google-apps-script

I'm using the current, "new" version of Google Forms. I have a form, Form A, and the associated script Script A. That script contains a function, onFormSubmit, associated with Form A's form submit trigger. The function receives one argument, an event, which contains the Form and the submitted FormResponse in the fields "source" and "response" respectively.
In the body of this function, how can I prevent the event/reject the form submission?
Alternatively, if this is not possible, how can I quietly prevent the FormResponse from being stored, or quietly remove it from the list of responses?
I see there is a Form.deleteAllResponses method. Do I have to delete all responses and then add all responses back again, except for the current one? Or is there a better way?

Try experimenting with the event trigger and use:
setAcceptingResponses(enabled)
Sets whether the form is currently accepting responses. The default for new forms is true.
Here is a code sample from a related SO post:
function onFormSubmit(){
var af = FormApp.getActiveForm();
var defaultClosedFor = af.getCustomClosedFormMessage();
af.setCustomClosedFormMessage("The form is currently processing a submission, please refresh the page.");
af.setAcceptingResponses(false);
<put your script stuff here>
af.setAcceptingResponses(true);
af.setCustomClosedFormMessage(defaultClosedFor);
}
Hope this helps.

The way I have handled this is by having a second sheet (tab within the same Google Sheet) into which Form responses are copied. The first thing that happens in the onFormSubmit() function is that the newest row in the responses sheet is copied to the duplicate sheet. You could implement a selection statement that chooses whether to make the copy or not depending on your criteria.
This means that there is always a copy of the raw responses from the form (important for auditing purposes) but also a means for correcting/modifying responses if errors were made by the submitter.
In case it is useful, this is my function that does the copy (note that my settings object is abstracted elsewhere but hopefully there is enough to make clear what is going on).
/**
* Copies form submissions from the responses sheet to another sheet.
*
* #param {event object} e the event object received from a form submit trigger
* #param {Settings} settings an object containing the settings this function expects to use
* #return {integer} the position of the new row in the destination sheet
*/
function copyFormSubmissionToSheet(e, settings) {
var sourceSheet = SpreadsheetApp.getActive().getSheetByName(settings.nameOfFormResponsesSheet);
var destinationSheet = SpreadsheetApp.getActive().getSheetByName(settings.name OfApprovalsSheet);
var newRow = e.range["rowStart"];
var columnCount = sourceSheet.getLastColumn();
var newResponseRange = sourceSheet.getRange(newRow, 1, 1, columnCount);
var newResponseDestinationRange = destinationSheet.getRange(destinationSheet.getLastRow()+1, 1, 1, columnCount);
newResponseRange.copyTo(newResponseDestinationRange);
var newDataSheetRow = destinationSheet.getLastRow();
return newDataSheetRow;
}

Related

Is it possible to use a single EditResponseURL to edit the same response pushed to 2 different spreadsheets?

There is a form "A" that is linked to spreadsheet "B". No issues there.
I have set up a script that pushes responses to spreadsheet "B" and a different spreadsheet "C".
However, the issues start to occur when a respondent wishes to edit their response by using the edit URL emailed to them. No issues occur with linked form "B" but anytime someone edits a response, it creates a new record in spreadsheet "C". I have tried many things, but cannot come up with a solution.
Sample code:
function onFormSubmit(e){
var form = FormApp.openById('form_id'); // Load form
var formResponses = form.getResponses();
var formResponse = formResponses[formResponses.length-1]; // last response
var itemResponses = formResponse.getItemResponses();
var ss = SpreadsheetApp.openById(ssID); // Second Spreadsheet
var dataSheet = ss.getSheetByName("Sheet1");
dataSheet.appendRow([x, y, z,]) // The form responses
}
Your code is pushing responses ONLY to spreadsheet C. Spreadsheet B is connected directly to your Form and is being populated with standard google forms functionality (for clarity I'm going to use terms SS Other for what you labeled C and SS Responses for the spreadsheet you label as B).
An edited response will keep the same response ID, which can be utilized to match if an existing response has been received. So in SS Other, you could create a response ID column, which you could check if an entry already exists.
To get the id of a response use:
var theID = formResponse.getId();
However, what I would do is just have SS Other just link to SS Response by using getRange function. Much easier, and no scripting.
Also, your method for grabbing the latest response isn't the best practice (though it will probably be fine over 99% of the time). I'm guessing you do that for testing purposes. I use a setup like below to address this. Ultimately a "real" submission and a testing one (which grabs latest) both feed into reviewResponse_ function with the response.
function entryMade(e) {
//using e.response ties to exact submission that was triggered
reviewResponse_(e.response);
}
function testEntryMade() {
//use this for test entries/debugging.
reviewResponse_(thisForm.getResponses()[thisForm.getResponses().length-1]);
}

is there a way to get form responses and place them in specific cell in another sheet

I have set up a script that creates a Google form and links it to a spreadsheet, is there a way to collect the responses and place them in certain cells in another sheet then unlinks and deletes the form response sheet.
the script I need will have to be flexible enough that it won't matter what the response sheet is named as I will be making multiple one use forms hence why I would also like to delete the response sheet after the answer is moved
for example say the answer is A (a1), B (b1) and C (c1) and I want to move it to sheet 'C' and into columns F, G and H after that's done i would like the response sheet to unlink and be deleted
any help would be greatly appreciated
Issue:
You want form response data to be submitted to a sheet of your choice, not the one that is created when linking the form to the spreadsheet.
Solution:
In that case, I'd suggest not linking the form to the spreadsheet at all, and use an onFormSubmit trigger to write the submitted data to your desired sheet.
Workflow:
Install an onFormSubmit trigger. You can do that manually, following these steps, or programmatically, by executing this function once:
const SOURCE_FORM_ID = "YOUR_FORM_ID"; // Change according to your needs
function installOnFormSubmitTrigger() {
const form = FormApp.openById(SOURCE_FORM_ID);
ScriptApp.newTrigger("onFormSubmitTrigger")
.forForm(form)
.onFormSubmit()
.create();
}
Once the trigger is installed, a function named onFormSubmitTrigger (it doesn't have to be named that way) will execute every time someone submits a response to the form. This function should append the response data to your desired sheet. It could be something like this (check inline comments):
const TARGET_SPREADSHEET_ID = "YOUR_SPREADSHEET_ID"; // Change according to your needs
const TARGET_SHEET_NAME = "Sheet1"; // Change according to your needs
function onFormSubmitTrigger(e) {
const targetSpreadsheet = SpreadsheetApp.openById(TARGET_SPREADSHEET_ID);
const targetSheet = targetSpreadsheet.getSheetByName(TARGET_SHEET_NAME);
if (targetSheet.getLastRow() === 0) { // Add headers if they don't exist yet
const itemTitles = e.source.getItems().map(item => item.getTitle()); // Get item titles
itemTitles.unshift("Timestamp"); // Append "Timestamp" to the sheet (if desired)
targetSheet.appendRow(itemTitles); // Append form item titles to the sheet
}
const itemResponses = e.response.getItemResponses();
const responses = itemResponses.map(itemResponse => itemResponse.getResponse()); // Get user responses
responses.unshift(new Date()); // Add today's date to the responses (if desired)
targetSheet.appendRow(responses); // Append responses to the sheet
}
Note:
If you don't want to submit to the first columns in the spreadsheet, simply add empty strings to the responses array, or use Range.setValues instead.
Reference:
Installable Triggers
appendRow

Ensure form submission trigger runs only one sheet

I have two sheets in a particular spreadsheet both of which have form submissions tied to them. The first sheet/form is supposed to send an email containing the form submission data (the original was created by Amit Agarwal, here's an historic link). The second form/sheet doesn't do anything special as it just collects data from the form. The script in question is set to a On Form Submit trigger.
The issue I am having is that the script sometimes runs from form/sheet2. I would like to specify which sheet/form the script needs to be triggered from to run on. The modified code that I have created was based on lots of looking around. Here is the snippet:
function Initialize() {
var triggers = ScriptApp.getProjectTriggers();
for (var i in triggers) {
ScriptApp.deleteTrigger(triggers[i]);
}
ScriptApp.newTrigger("SendConfirmationMail")
.forSpreadsheet(SpreadsheetApp.getActiveSpreadsheet())
.onFormSubmit()
.create();
}
function SendConfirmationMail(e) {
try {
var ss, bcc, sendername, subject, columns;
var message, value, textbody, sender;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Help Request Tickets');
var rowNumber = s.getActiveRange().getRowIndex();
var row = e.range.getRow();
// This is your email address and you will be in the BCC
bcc = "email", "email";
// This will show up as the sender's name
sendername = "sendername";
// Optional but change the following variable
// to have a custom subject for Google Docs emails
subject = "subject"
// This is the body of the auto-reply
message = "message"
ss = SpreadsheetApp.getActiveSheet();
columns = ss.getRange(1, 1, 1, ss.getLastColumn()).getValues()[0];
These two lines I though were supposed to accomplish this:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Sheet1');
I suppose the script thinks Sheet2 is active (which if I have the spreadsheet open could be the case). Surely there is way to work around/accomplish this, what I am missing?
A Sheets form submission trigger will be invoked for all forms submitted to the spreadsheet. Once upon a time, only one form could be associated with a spreadsheet, but now, with multiple form associations possible, you need to allow for that possibility. You can't specify which form a trigger function is for, but you can check the source of the event and respond appropriately.
One effective way to do this is to use a director function which will receive all form submission events, and direct them to unique trigger functions depending on which sheet received the response.
Here, we are associating "Form Responses 1" with SendConfirmationMail(), and assuming that "Form Responses 2" has its own form submission handler, handleForm2(). (If there is no handler for that form, then the specific case can be deleted, and submissions will end up in the default case.)
/**
* This director function should be used as the "top level" form submission trigger
* function for spreadsheets accepting responses from multiple forms. Events are
* directed to the appropriate trigger sub-functions according to the name of the
* sheet which received the current response.
*
* From: https://stackoverflow.com/a/37839189/1677912
*/
function formSubmitted(e) {
var sheetName = e.range.getSheet().getName();
switch (sheetName) {
case "Form Responses 1":
SendConfirmationMail(e);
break;
case "Form Responses 2":
handleForm2(e);
break;
default:
// do nothing
break;
}
}
If you use a Forms form submission trigger instead, you can avoid this altogether, since the destination spreadsheet would not be a direct consideration.
I suppose the script thinks Sheet2 is active (which if I have the spreadsheet open could be the case)
Not quite. The trigger function is invoked outside of the context of any spreadsheet UI, so what any user is doing in the spreadsheet has no effect on it. Rather, the "active" sheet is related to the submission event being handled. Regardless, it is a much better idea to reference the event object itself, rather than rely on "normal" operations. It does become trickier to test and debug, but not terribly so. For more about testing trigger functions, see How can I test a trigger function in GAS?

How to prevent Google Forms from converting form input to scientific notation format

I have a simple script set up that sends emails based on Google Form entries using a script-based VLookup to get the contact emails. In some cases, Google Forms converts longer numbers entered into the form field to scientific notation. A workaround I have been using is to enter an apostrophe before the number - for some reason this keeps the cell formatted to plaintext. I would like to find a solution that does not require this extra step.
The sheet has a form with a single field, eGCs. The eGCs field can contain ANY combination of letters and numbers and may be a multi-line string. The script sends an email to the user onFormSubmit with the eGCs field entry in the body of the email. The problem arises when I try to submit a very long string that is only numbers and the form entry variable is converted to scientific notation.
I need whatever the user enters in the eGCs field to appear EXACTLY as they entered it on both the Responses 1 sheet and in the body of the email that is sent. Here is the code:
function onFormSubmit(e) {
var eGCs = e.values[1];
var email = Session.getActiveUser().getEmail();
//Replace the Google Sheets formatted line breaks with HTML line breaks so they display properly in the email:
eGCs = eGCs.replace(/\n/g, '<br>');
//Send the email:
var subject = "This is only a test";
var body = eGCs;
MailApp.sendEmail(email, subject, body, {htmlBody: body})
return
}
If I submit
6110523527643880
...into the form, the number is changed to scientific notation format and appears as 6.11052E+15 both on the sheet and in the email that is sent. If I submit a multi-line string such as:
6110523527643880
6110523527643880
6110523527643880
...then the script works fine and the form field entry is not converted (probably because Google does not consider it a number any more). I need it to appear exactly as entered whether or not the form entry is a single line or multiple lines.
Here is my example sheet / script / form. It should be public, so please feel free to test it.
Form responses in Forms (as opposed to Spreadsheets) store responses as Strings. Your trigger function could grab the response from the form to get the string as entered by the respondent.
function onFormSubmit(e) {
// Get response sheet. First version works only in contained script,
// second works even in stand-alone scripts.
// var sheet = SpreadsheetApp.getActiveSheet();
var sheet = e.range.getSheet();
// Get URL of associated form & open it
var formUrl = sheet.getParent().getFormUrl();
var form = FormApp.openByUrl(formUrl);
// Get response matching the timestamp in this event
var timestamp = new Date(e.namedValues.Timestamp);
// NOTE: There is a race condition between the updates in Forms and Sheets.
// Sometimes (often!) the Spreadsheet Form Submission trigger function is invoked
// before the Forms database has completed persisting the new Responses. As
// a result, we might get no results when asking for the most recent response.
// To work around that, we will wait and try again.
var timeToGiveUp = 0;
do {
if (timeToGiveUp > 0) Utilities.sleep(1000); // sleep 1s on subsequent tries
timeToGiveUp++;
var responses = form.getResponses(timestamp);
} while (responses.length == 0 && (timeToGiveUp < 3));
Logger.log("time to give up "+timeToGiveUp);
var response = responses[0]; // assume just one response matches timestamp
var itemResponses = response.getItemResponses();
var eGCsItemNumber = 1; // Indicates where the question appears in the form
var eGCs = itemResponses[eGCsItemNumber-1].getResponse().toString();
// You now have exactly what the respondent typed, as a string.
// It can be used as-is in an email, for example.
var body = "The user entered: "+eGCs;
MailApp.sendEmail(
Session.getActiveUser().getEmail(),
"This is only a test",
body
);
// To preserve the value in the spreadsheet, we must
// force it to remain a string by prepending a tick (')
var eGCsCol = 2;
e.range.offset(0,eGCsCol-1,1,1).setValue("'"+eGCs);
}
Note wrt Race condition comment: The actual work that was needed in this area of the code was a single line:
var responses = form.getResponses(timestamp);
While playing with this, I found that I was frequently receiving an exception, the same as noted in comments below this answer...
Cannot find method getResponses(object)
It turned out that this only happened when the function was triggered by a form submission event, not when running from the editor/debugger with simulated events. That implies that, for a short period of time, the response we're trying to handle is not returned by the call to getResponses().
Because of the way that shared documents are implemented, there is a propagation delay for any change... that's the time it takes for a change in one view of an asset to propagate to all other views.
In this situation, our trigger function has launched with a spreadsheet event, and then opens a view of the Form and tries to read the newest responses before that view contains them.
A simple work-around would be to sleep() for a period of time that would allow propagation to complete.
Utilities.sleep(5000); // 5s pause
var responses = form.getResponses(timestamp);
Simple, yes - but inefficient, because we'd be waiting even when we didn't need to. A second problem would be determining how long was long enough... and what if that changed tomorrow?
The chosen work-around will retry getting responses only if it is not successful the first time. It will only wait when doing a retry. And it won't wait forever - there's a limiting condition applied, via timeToGiveUp. (We could have added an additional check for success after the loop, but since the next statement will through an exception if we've blown through our time limit, we can let it do the dirty work.)
var timeToGiveUp = 0;
do {
if (timeToGiveUp > 0) Utilities.sleep(1000); // sleep 1s on subsequent tries
timeToGiveUp++;
var responses = form.getResponses(timestamp);
} while (responses.length == 0 && (timeToGiveUp < 3));
Lots more than one line of code, but more robust.
I am assuming eGCs is the response with the number.
e.values[2] will always come back as a string (in this case "6.15312E+16"), therefore you cannot convert that to the original number as you loose everything after that last 2. Even if you convert it, the best you can get is "61531200000000000"
Instead, you can pull the value from the spreadsheet.
In the beginning of your onFormSubmit() function add this code:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Form Responses 1");
var eGCs = sheet.getRange(sheet.getLastRow(), 2, 1, 1).getValue();
try {
eGCs = eGCs.toFixed();
} catch (e) {
Logger.log(eGCs);
}
Your eGCs will now return as the full number if it's a number, otherwise it will return as text.
If you want the spreadsheet to have the right format when new responses are submitted add this to your code:
sheet.getRange(sheet.getLastRow(), 2, 1, 1).setNumberFormat("000");
This will convert the new row added by the form in to the correct format. This does not affect the actual value in the spreadsheet, only the way it is formatted.

Script to copy and sort form submission data

I'm using Google forms to create a spreadsheet that I want sorted automatically by datestamp Z-A. The sorting will be triggered whenever anyone fills out a form.
I think the way to do it is:
ask if there is a "copy of Form responses" on spreadsheet...
if yes, clear all contents...
else...
copy spreadsheet to "copy of form responses"...
sort according to timestamp
Below is what I've cobbled so far. It works only the first time a response is recorded. I'm not a coder so any help is appreciated. If someone could also point me to a list of commands with basic syntax I'd be grateful.
function CopySheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var msheet = ss.getSheetByName("Form Responses");
msheet.copyTo(ss);
var CopySheet = ss.getSheetByName("Copy of Form Responses");
CopySheet.sort(1, false); // here 1 is for column no. 1 that
// is "Column A" and true is for ascending, make it
// false if you want descending.
};
You can accomplish this without a script, by using QUERY() in the copy sheet. For instance, if you put this function in cell A1 of your copy sheet, and substitute the key for your form response spreadsheet, you'll end up with a reverse-timestamp-sorted copy of the responses:
=Query(ImportRange(spreadsheet_key,"Form Responses!A:Z"), "select * order by Col1 desc")
This data will be refreshed periodically (~5 mins), so it will reflect new form submissions, but not in real-time.
Note: When using ImportRange() as source data for Query, you need to refer to columns in the Query string using ColN notation.
Alternatively, you could produce a form submission trigger function in the spreadsheet receiving the form submissions, and have it copy the sorted form responses to your copy sheet. The following function does that. You need to set it up as a trigger function for Spreadsheet Form Submission Events. For information on how to test such a function, see How can I test a trigger function in GAS?.
function copyFormSubmissions(e) {
var sourceSheet = e.range.getSheet();
var data = sourceSheet.getDataRange().getValues();
var headers = data.splice(0,1)[0]; // remove headers from data
data.sort(reverseTimestampOrder); // Sort 2d array
data.splice(0,0,headers); // replace headers
var destId = "--copy-sheet-ID--";
var destSheet = SpreadsheetApp.openById(destId).getSheetByName('Sheet1');
destSheet.clear();
destSheet.getRange(1,1,data.length,data[0].length).setValues(data);
};
function reverseTimestampOrder(a,b) {
// Timestamp is in first (zero-th) column
return (b[0]-a[0]);
}
If someone could also point me to a list of commands with basic syntax I'd be grateful.
The Google Apps Script API classes and methods reference is here. If you're learning, try the tutorials (same place), and I recommend you get familiar with Javascript through some form of e-learning - CodeAcademy.com is a good place to start, since it introduces all the language constructs without focusing on web page development, making it very relevant for Googls Apps Script.