Google Sheets Form Response Conditional Responses - javascript

I'm relatively new to coding, especially in Javascript. I'm working on a google form that links to a google sheet and I want a conditional notification that when they answer a certain response it will notify that departments manager via email.
Essentially, I need my script to search for the value of a cell that contains the email to send.
I'm using a VLOOKUP chart to automatically input the email of that department based on the the department question they answer.
Additionally I need a way to trigger this function to run when a new response is entered. Making sure that only an email is sent for the new response and not any of the older ones.
enter image description here
My email will have the message line and subject using this code below:
var recipient = emailAddress
var message = ''
var subject = 'Department Alert';
MailApp.sendEmail(emailAddress, subject, message);
Any thoughts on how to achieve this?

This code will do the work for you.
Install the trigger onFOrmSubmit. Then it should be good to go.
function onFormSubmit(e) {
const form = FormApp.getActiveForm();
const items = form.getItems();
const formResponses = e.response;
const arr = [];
let receiptant_name;
for (i in items) {
let each_response = formResponses.getResponseForItem(items[i]).getResponse();
// this column has the supervisor name, you can change column title into yours.
if (items[i].getTitle() == 'Supervisor Name') {
// return a list of name
receiptant_name = each_response;
}
if (typeof(each_response) == "object") {
each_response ='<div>' + each_response.map(r => { return '<div>'+r+'</div>';}).join('') + '</div>';
}
const title = items[i].getTitle();
arr.push([title, each_response]);
}
const emailbody = return_emailbody(arr);
const receiptant_email = return_supv_email(receiptant_name);
sendEmail(receiptant_email, emailbody);
}
//this function to map form responses into html table.
function return_emailbody(responsearray) {
let mail_array = [];
responsearray.forEach(r => {
if (r[1] === null || r[1] === '') {
r[1] = 'N/A';
}
mail_array.push('<tr><td>' + r[1] + '</td></tr>')
});
const competed_template = htmltemplate(mail_array.join(''));
return competed_template;
}
//sheet id contains the supervisor email.Default Sheet1, you can change to your sheet name.
function return_supv_email(sheetid,supv_name) {
//receive name list
const values = sheet_data_return(sheetid, 'Sheet1');
let email_list = [];
for(i in supv_name){
values.filter(record => {
if (record[1].replace(/\s/g, '').includes(supv_name[i].replace(/\s/g, ''))) {
email_list.push (record[2]);
}
});
}
return email_list.toString();
}
function sheet_data_return(id, sheet_Name) {
return SpreadsheetApp.openById(id).getSheetByName(sheet_Name).getDataRange().getDisplayValues();
}
function sendEmail(recipient, emailbody) {
GmailApp.sendEmail(
recipient,
'FORM GOT A NEW RESPONSE',
emailbody,
{
htmlBody: emailbody,
name: 'GOOGLE FORM RESPONSE',
});
}
//you can edit your this HTML template to make your email look better.
function htmltemplate(emailbody) {
let template = '<!DOCTYPE html> <html> <head> <base target="_top"> </head><body><table>{body}</table> </body> </html>';
template = template.replace('{body}', emailbody);
return template;
}

Related

How to run certain scripts under the authority of a specific user?

I was able to allow other users to add a new SKU to a sheet without unprotecting it (Original post). Now I am trying to do the inverse, to allow users to delete an SKU without unprotecting the sheet.
I started with the following, which works as expected:
function deleteEachRow(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Enter New SKU:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
SHEET.deleteRow(i+1);
};
};
};
};
I tried to Frankenstein the above code into the answer I was provided. Now the script runs without error but fails to delete the entered SKU as expected. This is the script I am running:
function deleteEachRow1(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Delete Which SKU?:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
const webAppsUrl = "WEB APP URL"; // Pleas set your Web Apps URL.
const url = webAppsUrl + "?text=" + text;
const res = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
// ui.alert(res.getContentText()); // You can see the response value using this line.
}
}
function doGet(e) {
const text = e.parameter.text;
const sheet = SpreadsheetApp.getActive().getSheetByName('Ordering');
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
SHEET.deleteRow(i+1);
};
};
myFunction();
return ContentService.createTextOutput(text);
}
// This script is from https://tanaikech.github.io/2017/07/31/converting-a1notation-to-gridrange-for-google-sheets-api/
function a1notation2gridrange1(a1notation) {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
};
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
}
// Please run this function.
function myFunction() {
const email = "MY EMAIL"; // <--- Please set your email address.
// Please set your sheet names and unprotected ranges you want to use.
const obj = [
{ sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },
{ sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },
{ sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },
{sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] },
{sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
];
// 1. Retrieve sheet IDs and protected range IDs.
const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges: obj.map(({ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push(protectedRanges.map(({ protectedRangeId }) => protectedRangeId));
o.sheetIds.push(sheetId);
return o;
}, { protectedRangeIds: [], sheetIds: [] });
// 2. Convert A1Notation to Gridrange.
const gridranges = obj.map(({ sheetName, unprotectedRanges }, i) => unprotectedRanges.map(f => a1notation2gridrange1(`${sheetName}!${f}`)));
// 3. Create request body.
const deleteProptectedRanges = protectedRangeIds.flatMap(e => e.map(id => ({ deleteProtectedRange: { protectedRangeId: id } })));
const protects = sheetIds.map((sheetId, i) => ({ addProtectedRange: { protectedRange: { editors: {users: [email]}, range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
// 4. Request to Sheets API with the created request body.
Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}
Probably the easiest way to do this would be to avoid using a button and using a checkbox with a installable edit trigger, which also has a great side effect of mobile support.
Proposed solution:
Using a checkbox
Hook it to a installable edit trigger, which runs as the user who installed the trigger. Therefore, if the owner installs the trigger, no matter who edits the sheet, the trigger runs as the owner, giving access to privileged resources including protected ranges.
The installable version runs with the authorization of the user who created the trigger, even if another user with edit access opens the spreadsheet.
Notes:
Advantage:
Code simplicity and maintainabilty. No need for webapp or any complicated setup.
Disadvantage: Security (with possible workaround)
If the code is bound to the sheet, editors of the sheet get direct access to the script of the sheet. So, any editor with malicious intentions would be able to modify the code. If the function with installable trigger has gmail permissions, any editor would be able to log all the emails of the owner. So,special attention needs to be paid to permissions requested. Note that, this is already the case with your web app setup. Any editor maybe able to modify doGet to access protected data. If the webapp is in a separate standalone script, this isn't a issue. You may also be able to fix this issue by setting the trigger at a predetermined version instead of Head version. See this answer for more information.

Google Sheets Scripts - run scripts as administrator / owner

I have Google Sheet, name TEST https://docs.google.com/spreadsheets/d/1HsRwknyZBmZZ9nibDfNpOwqkVsFGThDyrTwspV-5_4U/edit?usp=sharing
Sheet: Arkusz 1
Column A: all people can edit
Column B: only owner can edit
Library (for everyone): https://script.google.com/macros/s/AKfycbzpnEMhIG-0dMp54q3W4UxoT71-lSdfF7Qxf7rq_j6gJMNIxuCS/exec
A user cannot add a row because it is blocked by column B, which belongs only to the admin.
How can I create macro, which allow user to add new rows?
I have three scripts:
function insertRow() {
var ss = SpreadsheetApp.getActive()
var sheetName = ss.getActiveSheet().getName()
var row = ss.getActiveRange().getRow()
var numRows = Browser.inputBox('Insert Rows', 'Enter the number of rows to insert', Browser.Buttons.OK);
Logger.log(numRows)
var url ="https://script.google.com/macros/s/AKfycbzpnEMhIG-0dMp54q3W4UxoT71-lSdfF7Qxf7rq_j6gJMNIxuCS/exec"
var queryString = "?sheetName="+sheetName+"&rowNo="+row+"&noOfRows="+numRows
url = url + queryString
Logger.log(url)
var request = UrlFetchApp.fetch(url)
if (request != 'Success')
Browser.msgBox(request)
}
Second:
function doGet(e) {
var param = e.queryString
var parameters = param.split("&")
// This just checks only 3 parameters are present else gives a invalid link
if (param != null && parameters.length == 3){
param = e.parameter
var name = param.sheetName
var row = Number(param.rowNo)
var numOfRows = Number(param.noOfRows)
} else{
return ContentService.createTextOutput("Invalid query")
}
try{
var ss = SpreadsheetApp.openById("https://docs.google.com/spreadsheets/d/1HsRwknyZBmZZ9nibDfNpOwqkVsFGThDyrTwspV-5_4U")
var sheet = ss.getSheetByName(name)
sheet.insertRowsAfter(row, numOfRows);
var source_range = sheet.getRange(row,1,1,sheet.getLastColumn());
var target_range = sheet.getRange(row+1,1,numOfRows);
source_range.copyTo(target_range);
}
catch (err){
return ContentService.createTextOutput("error: "+err)
}
return ContentService.createTextOutput("Success")
}
And after clicked function insertRow and filled number of rows I have doPost(e) information.
Could you help me?
On the solution you provided below, I see that the issue is in mainScript
function mainScript(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
// assign the sheet to a variable and use it below instead of spreadsheet
var sheet = spreadsheet.getSheetByName('ZNC')
sheet.getRange('A2').activate()
sheet.insertRowsBefore(sheet.getActiveRange().getRow(), 1);
}
Hmm, I created solution, but I think there's a bug somewhere, because it doesn't add the line, even though everything is correct and the script is published as public.
function ZNCWiersz() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('ZNC'), true);
const activeSheet = SpreadsheetApp.getActiveSheet().getSheetName();
const url = ScriptApp.getService().getUrl();
UrlFetchApp.fetch(`${url}?sheetName=${activeSheet}`, {
headers: { authorization: "Bearer " + ScriptApp.getOAuthToken() },
});
// DriveApp.getFiles() // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive.readonly". This scope is used for the access token.
}
// When runScript() is run, this function is run.
const doGet = (e) => ContentService.createTextOutput(mainScript(e));
// This script is run by Web Apps.
function mainScript(e) {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
spreadsheet.getSheetByName('ZNC')
spreadsheet.getRange('A2').activate()
spreadsheet.insertRowsBefore(spreadsheet.getActiveRange().getRow(), 1);
}

how to getRange of more than one cell in same row in a script

Im trying to make a script that sends email alert when specific fields reaches certain values. It works fine for one field, but how Do I do it so it sends alert when any of the fields in specific row range reaches that value.
Im using this code:
function CheckSales() {
// Tikrinam laukelio value
var monthSalesRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("reportai").getRange("A15");
var monthSales = monthSalesRange.getValue();
// tikrinam value
if (monthSales < 200){
// pasiimam email adresa
var emailRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1").getRange("B2");
var emailAddress = emailRange.getValue();
// Siuncima email.
var message = 'Mazas likutis: ' + monthSales; // Second column
var subject = 'Mazas Likutis';
MailApp.sendEmail(emailAddress, subject, message);
}
}
I tried using .getRange("A15:E15") but still it send alert only when A15 reaches value less than 200, its not reacting to B15, C15 ect.
You need to iterate the values in A15:E15 with a loop. Try this:
function checkSales() {
const monthSalesRange = SpreadsheetApp.getActive().getRange('reportai!A15:E15');
const emailAddress = SpreadsheetApp.getActive().getRange('Sheet1!B2').getValue();
const subject = 'Mazas Likutis';
let message = 'Mazas likutis: ';
const alerts = [];
monthSalesRange.getValues().flat()
.forEach(sales => {
if (Number(sales) && sales < 200) {
alerts.push(sales);
}
});
if (alerts.length) {
MailApp.sendEmail(emailAddress, subject, message + alerts.join(', '));
}
}
Some of the best resources for learning Google Apps Script include the Beginner's Guide, the New Apps Script Editor guide, the Fundamentals of Apps Script with Google Sheets codelab, the Extending Google Sheets page, javascript.info, Mozilla Developer Network and Apps Script at Stack Overflow.

JavaScript: Catch errors in async function and stop the rest of the function if there are any

EDIT: ANSWER BELOW
I'm making my first JavaScript project and decided to make a simple weather app. It fetches weather data of a city you put in from the openweathermap.org api and displays it in a table. I firstly made it using fetch() and .then. I then learned about async functions and the await keyword. After converting the script to an asynchronous function, I came across a problem. If the first city you enter isn't a real city (an error is catched while fetching the api), the warning message appears, BUT the table also appears because the rest of the function still executes.
So my question is: how can I stop the async function if any errors are catched?
Here's the website: https://lorenzo3117.github.io/weather-app/
Here's the code:
// Launch weather() function and catch any errors with the api request and display the warning message if there are any errors
function main() {
weather().catch(error => {
document.querySelector("#warningMessage").style.display = "block";
console.log(error);
});
}
// Main function
async function weather() {
// Take city from input and reset input field
var city = document.querySelector("#cityInput").value;
document.querySelector("#cityInput").value = "";
// Get api response and make it into a Json
const apiResponse = await fetch("https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=<apiKey>&units=metric");
const jsonData = await apiResponse.json();
// Removes warning message
document.querySelector("#warningMessage").style.display = "none";
// Puts the Json into an array and launches createTable function
var arrayJson = [jsonData];
createTable(document.querySelector("#table"), arrayJson);
// Function to create the table
function createTable(table, data) {
// Makes the table visible
document.querySelector("#table").style.display = "block";
// Goes through the array and makes the rows for the table
for (let i = 0; i < data.length; i++) {
let rowData = data[i];
var row = table.insertRow(table.rows.length);
// This var exists to make the first letter capitalized without making a gigantic line (see insertCell(3), line 53)
// Could be made into a function if needed
var weatherDescription = rowData.weather[0].description;
// Take latitude and longitude for google maps link
var lat = rowData.coord.lat;
var long = rowData.coord.lon;
// Make an a-tag for link to google maps
var mapLink = document.createElement("a");
mapLink.innerHTML = "Link";
mapLink.target = "_blank";
mapLink.href = "https://www.google.com/maps/search/?api=1&query=" + lat + "," + long;
// Making rows in table
row.insertCell(0).innerHTML = rowData.name + ", " + rowData.sys.country;
row.insertCell(1).innerHTML = rowData.main.temp + " °C";
row.insertCell(2).innerHTML = rowData.main.humidity + "%";
row.insertCell(3).innerHTML = weatherDescription.charAt(0).toUpperCase() + weatherDescription.slice(1);
row.insertCell(4).appendChild(mapLink); // appendChild for anchor tag because innerHTML only works with text
}
}
And the repo: https://github.com/lorenzo3117/weather-app
Thank you
you can do this :
async function weather() {
try {
const apiResponse = await fetch("https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=02587cc48685af80ea225c1601e4f792&units=metric");
} catch(err) {
alert(err); // TypeError: failed to fetch
return;
}
}
weather();
Actually, the error catched isn't an error with the api itself because the api still sends a json, but the error is catched while trying to read a certain object from the json (which doesn't exist because the json isn't a normal one with weather data). Therefore the function stops far later than expected, after the table was made visible.
I just put the line that made the table visible after the function that creates the table (after where the real error occurs). Also thanks #Dadboz for the try catch method which made the code even more compact. I also added an if else to check if the json file is the correct one so unnecessary code doesn't get executed. Thanks #James for pointing this out to me.
Here's the final code:
// Main function
async function weather() {
try {
// Take city from input and reset input field
var city = document.querySelector("#cityInput").value;
document.querySelector("#cityInput").value = "";
// Get api response and make it into a Json
const apiResponse = await fetch("https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=<apiKey>&units=metric");
const jsonData = await apiResponse.json();
if (jsonData.message == "city not found") {
document.querySelector("#warningMessage").style.display = "block";
} else {
// Removes warning message
document.querySelector("#warningMessage").style.display = "none";
// Puts the Json into an array and launches updateTable function
var arrayJson = [jsonData];
updateTable(document.querySelector("#table"), arrayJson);
}
}
catch (error) {
console.log(error);
}
}
// Function to update the table
function updateTable(table, data) {
// Goes through the array and makes the rows for the table
for (let i = 0; i < data.length; i++) {
let rowData = data[i];
var row = table.insertRow(table.rows.length);
// This var exists to make the first letter capitalized without making a gigantic line (see insertCell(3), line 53)
// Could be made into a function if needed
var weatherDescription = rowData.weather[0].description;
// Take latitude and longitude for google maps link
var lat = rowData.coord.lat;
var long = rowData.coord.lon;
// Make an a-tag for link to google maps
var mapLink = document.createElement("a");
mapLink.innerHTML = "Link";
mapLink.target = "_blank";
mapLink.href = "https://www.google.com/maps/search/?api=1&query=" + lat + "," + long;
// Making rows in table
row.insertCell(0).innerHTML = rowData.name + ", " + rowData.sys.country;
row.insertCell(1).innerHTML = rowData.main.temp + " °C";
row.insertCell(2).innerHTML = rowData.main.humidity + "%";
row.insertCell(3).innerHTML = weatherDescription.charAt(0).toUpperCase() + weatherDescription.slice(1);
row.insertCell(4).appendChild(mapLink); // appendChild for anchor tag because innerHTML only works with text
}
// Makes the table visible
document.querySelector("#table").style.display = "block";
}
Thanks everyone for your answers, have a good day!
Lorenzo

Pass parameter in bbUI to another page failed

Based On : Blackberry Wiki Screen
With my full code : My Full Code
If i user onscreenready function to PASS MY PARAMETER from page A like this :
ondomready: function(element, id) {
if (id == 'UPDATE') {
UPDATE_initialLoad(element, params);
}
}
For pages that I would give / throw parameters (Let's just say page B), how do I win or get results / parameters provided by the previous form. I want to make a form update the data in the form, and will be filled all the time throwing fieldnya parameter data.
Im using like this in my Page B :
function UPDATE_initialLoad(element, params) {
setTimeout(UPDATE_loadAfterTimeout,100);
}
function UPDATE_loadAfterTimeout() {
var id = data_id
var nmDepan = data_depan
var nmBelakang = data_belakang
var phone = data_phone
var email = data_email
document.getElementById('txtID').value = id
document.getElementById('txtNMDEPAN').value = nmDepan
document.getElementById('txtNMBELAKANG').value = nmBelakang
document.getElementById('txtPhone').value = phone
document.getElementById('txtEmail').value = email
}
But result is failed...
Please help me
You'll want to push data to the page:
bb.pushScreen("myPage.html", "UPDATE", myParams);
Then this will fire when the page is loaded:
ondomready: function(element, id, params) {
if (id == 'UPDATE') {
UPDATE_initialLoad(element, params);
}
}
Make sure to pass the parameters to your delayed function:
function UPDATE_initialLoad(element, params) {
setTimeout(UPDATE_loadAfterTimeout(params),100);
}
function UPDATE_loadAfterTimeout(params) {
var id = params['data_id'];
var nmDepan = params['data_depan'];
var nmBelakang = params['data_belakang'];
var phone = params['data_phone'];
var email = params['data_email'];
document.getElementById('txtID').value = id
document.getElementById('txtNMDEPAN').value = nmDepan
document.getElementById('txtNMBELAKANG').value = nmBelakang
document.getElementById('txtPhone').value = phone
document.getElementById('txtEmail').value = email
}
I hope this helps.

Categories