Trigger on calculated date on Google AppMaker - google-apps-script

I have function called alertnotice(to, message, body) that will be executed on a user onClick() event. The function will execute sendEmail(to, message, body) to send the email base on a calculated trigger as in variable triggerDay such as below:
function alertnotice(to, subject, body, dueDate, notifyBeforeDay) {//start of this class
function sendEmail(to, subject, body){
try {
MailApp.sendEmail({
to: to,
subject: subject,
htmlBody: body
});
} catch(e) {
console.error(JSON.stringify(e));
}
}
//check if there is an existing trigger for this process
var existingTrigger = PropertiesService.getScriptProperties().getProperty("sendEmailTrigger");
//set the renewal notice day
var triggerDay = (dueDate - notifyBeforeDay) / (1000*60*60*24);
//if the trigger already exists, inform user about it
if(existingTrigger) {
return "Alert notice had been sent";
} else { // if the trigger does not exists, continue to set the trigger to send alert notice
//runs the script every day at 1am on the time zone specified
var newTrigger = ScriptApp.newTrigger('sendEmail')
.timeBased()
.atTime(triggerDay)
.create();
var triggerId = newTrigger.getUniqueId();
if(triggerId) {
PropertiesService.getScriptProperties().setProperty("autoExportTrigger", triggerId);
return "Alert notice send successfully!";
} else {
return "Failed to send alert notice. Try again please";
}
}
}//end of this class
So for example, if the dueDate is 30/07/2018 and the notifyBeforeDay = 30, the function should send the email 30 days before the due date. I tried to achieve that but not so sure whether my algorithm will work. Can anyone give advice on this?

This implementation looks fragile to me. I would rather go with single trigger to avoid any possible duplicated emails and ensure at least one. Smth like this:
// I am server script, trigger me on schedule (for instance nightly)
function sendAlerts() {
var query = app.models.Customers.newQuery();
// query only for non-notified customers with upcoming due date
query.filters.NorificationAttempts._lessThan = ATTEMPTS_THRESHOLD;
query.filters.Notified._equals = false;
query.filters.DueDate._lessThan = <dueDate>;
var customers = query.run();
customers.forEach(function(customer) {
var success = sendNotification(customer);
if (success) {
customer.Notified = true;
} else {
customer.NorificationAttempts++;
}
// performance trade off in favor of consistency
app.saveRecords([customer]);
});
}
Trigger for such script can be installed by app's admin, you can find similar implementation in People Skills template.

Related

Using an older deployment after update

I'm trying to make a wedding website using this person's blog/github: https://blog.rampatra.com/wedding-website. I had this to where when someone filled out the form on my website, I received an email and my google spreadsheet updated. Then we needed a shorter invite code, so I changed the logic. Now, when I enter the new code, I get an error message saying the new code is invalid and the old code still works even though anything other than the new code should be throwing an error.
I have an HTML form on my index.html exactly like the original.
I also have a section in my scripts.js that was identical to the example:
/********************** RSVP **********************/
$('#rsvp-form').on('submit', function (e) {
e.preventDefault();
var data = $(this).serialize();
$('#alert-wrapper').html(alert_markup('info', '<strong>Just a sec!</strong> We are
saving your details.'));
if (MD5($('#invite_code').val()) !== 'b0e53b10c1f55ede516b240036b88f40'
&& MD5($('#invite_code').val()) !== '2ac7f43695eb0479d5846bb38eec59cc') {
$('#alert-wrapper').html(alert_markup('danger', '<strong>Sorry!</strong> Your
invite code is incorrect.'));
} else {
$.post
('https://script.google.com/macros/s/<my deployment id>-/exec', data)
.done(function (data) {
console.log(data);
if (data.result === "error") {
$('#alert-wrapper').html(alert_markup('danger', data.message));
} else {
$('#alert-wrapper').html('');
$('#rsvp-modal').modal('show');
}
})
.fail(function (data) {
console.log(data);
$('#alert-wrapper').html(alert_markup('danger', '<strong>Sorry!</strong>
There is some issue with the server. '));
});
}
});
For my google sheet, I used the example script and initially only changed the email address
var TO_ADDRESS = "youremailaddress#gmail.com"; // email to send the form data to
/**
* This method is the entry point.
*/
function doPost(e) {
try {
Logger.log(e); // the Google Script version of console.log see: Class Logger
var mailData = e.parameters; // just create a slightly nicer variable name for the
data
if (mailData.invite_code != "271117") { // validate invite code before saving data
Logger.log("Incorrect Invite Code");
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "message": "Sorry, your
invite code (" + mailData.invite_code + ") is incorrect."}))
.setMimeType(ContentService.MimeType.JSON);
}
record_data(e);
MailApp.sendEmail({
to: TO_ADDRESS,
subject: "A new guest RSVP'd for your wedding",
replyTo: String(mailData.email), // This is optional and reliant on your form
actually collecting a field named `email`
htmlBody: formatMailBody(mailData)
});
return ContentService // return json success results
.createTextOutput(JSON.stringify({"result":"success","data":
JSON.stringify(e.parameters) }))
.setMimeType(ContentService.MimeType.JSON);
} catch(error) { // if error return this
Logger.log(error);
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "message": "Sorry, there is
an issue with the server."}))
.setMimeType(ContentService.MimeType.JSON);
}
}
/**
* This method inserts the data received from the html form submission
* into the sheet. e is the data received from the POST
*/
function record_data(e) {
Logger.log(JSON.stringify(e)); // log the POST data in case we need to debug it
try {
var doc = SpreadsheetApp.getActiveSpreadsheet();
var sheet = doc.getSheetByName('responses'); // select the responses sheet
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [ new Date().toUTCString() ]; // first element in the row should always
be a timestamp
// loop through the header columns
for (var i = 1; i < headers.length; i++) { // start at 1 to avoid Timestamp column
if(headers[i].length > 0) {
row.push(e.parameter[headers[i]]); // add data to row
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
}
catch(error) {
Logger.log(error);
Logger.log(e);
throw error;
}
finally {
return;
}
}
After I got the example working, I changed my Google AppScript to if(mailData.invite_code !="271117") to a new code and then hit deploy -> new deployment. I copied the new deployment ID and hit deploy. The new version seemed to deploy.
In scripts.js I changed:
(MD5($('#invite_code').val()) !== 'b0e53b10c1f55ede516b240036b88f40'
to the hash of the new code and
$.post
('https://script.google.com/macros/s/<my deployment id>-/exec', data)
to the new deployment id
I saved my code with the new deployment id, committed, and ran terraform apply and updated the S3 bucket. I went back to my website and completed the RSVP form again.
The website changes URL to one that has the json response and says "Sorry, your invite code (mycode) is incorrect". That code matches the code in my script exactly. If I fill out a new RSVP with the code from the example, it works as if I didn't change anything. When I check the 'executions' table in Apps Scripts, the deployment version kicked off is the older one.
Do I need to archive the old deployment? Is there a delay, and if there is a delay why isn't my scripts.js failing?

Issue running an Installed Trigger in Google App Script

Fairly new to app script so bare with me.
Wrote this massive script, then went to set it up on a times trigger and it just refuses to run. I've gone ahead an back tracked as much as I could, to get at least something to work, yet I can't even get a basic toast to appear on a minute interval.
This is the script I've built, which I'm running directly to enable the trigger:
function createTriggers() {
ScriptApp.newTrigger('testTime')
.timeBased()
.everyMinutes(1)
.create();
};
The function it's calling is super simple, I've used it a lot and change it a lot too:
var gSS = SpreadsheetApp.openById("this_1s/the.1d")
function testTime() {
var d = new Date()
var Start = d.getTime();
gSS.toast(Start, "Testing", 30)
};
So how it should work, and it does if I just call the 'testTime' function directly, is a little toast pop-up appears on the spreadsheet in question, and stays visible for 30s.
When I run the trigger function 'createTriggers', nothing happens..
Please help! All the code I wrote is for nothing if I can't get it to run on its own.. :(
***** EDIT - 08/04/20 - based on comments *****
It's possible this was an XY example, I tried to run a small segment of the original code which works when I run it directly, and its not working here either.. this snippit does not have any UI facing functions in it, so it shouldn't be the issue..
All i did was take the above trigger function and change the name to 'testClear', which calls to the following functions:
function testClear(){
sheetVars(1)
clearSheetData(sheetSPChange)
};
function sheetVars(numSprints) {
// returns the global vars for this script
try {
sheetNameSprints = "Name of Sprint Sheet"
sheetNameSPChange = "Name of Story Point Change Sheet"
sheetSprints = gSS.getSheetByName(sheetNameSprints)
sheetSPChange = gSS.getSheetByName(sheetNameSPChange)
arraySprints = iterateColumn(sheetSprints,"sprintIDSorted", 1, numSprints)
}
catch(err) {
Logger.log(err)
};
};
function iterateColumn(sheet, header, columnNum, numRows) {
// Create an array of first column values to iterate through
// numRows is an int, except for the string "all"
var gData = sheet.getDataRange();
var gVals = gData.getValues();
var gLastR = ""
var gArray = []
// check how many rows to iterate
if (numRows == "all") {
gLastR = gData.getLastRow();
}
else {
gLastR = numRows
};
// Iterate through each row of columnNum column
for (i = 1; i < gLastR; i++){
// iterate through
if(gVals[i][columnNum] !== "" && gVals[i][columnNum] !== header){
// push to array
gArray.push(gVals[i][columnNum]);
}
else if (gVals[i][columnNum] == "") {
break
};
};
return gArray
};
function clearSheetData(sheet) {
// Delete all rows with data in them in a sheet
try {
if (!sheet.getRange(sheet.getLastRow(),1).isBlank()){
sheet.getRange(2, 1, sheet.getLastRow()-1, sheet.getLastColumn()-1).clearContent()
Logger.log("Sheet cleared from old data.")
}
else {
sheet.deleteRows(2, sheet.getLastRow()-1)
Logger.log("Sheet rows deleted from old data.")
};
}
catch(err){
Logger.log(err)
emailLogs()
};
};
The 'emailLogs' function is a basic MailApp so i get notified of an issue with the script:
function emailLogs() {
// Email Nikita the loggs of the script on error
var email = "my work email#jobbie"
var subject = "Error in Sheet: " + gSS.getName()
var message = Logger.getLog()
MailApp.sendEmail(email, subject, message)
};
Thanks to a comment I've now discovered the executions page!! :D This was the error for the edited script.
Aug 4, 2020, 10:48:18 AM Error Exception: Cannot call
SpreadsheetApp.getUi() from this context.
at unknown function
To show a toast every certain "time" (every n iterations) add this to the for loop
if (!((i+1) % n)) spreadsheet.toast("Working...")
From the question
Aug 4, 2020, 10:48:18 AM Error Exception: Cannot call SpreadsheetApp.getUi() from this context. at unknown function
The above error means that your time-drive script is calling a method that can only be executed when a user has opened the spreadsheet in the web browser.
In other words, toast can't be used in a time-driven trigger. The solution is to use client-side code to show that message every minute. To do this you could use a sidebar and a recursive function that executes a setTimeout
References
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals
Based on all the comments, and new things I'd learned from that..:
I'd been calling to a global variable for my onOpen function:
var gUI = SpreadsheetApp.getUi();
even though I wasn't using it for the trigger, since it was global it tried to run and then failed.
I moved the actual definition of gUI into the onOpen function, and tried again and it worked.
Thank you all for the support!

Google App Maker: date-driven email notifications

I need to set up email notifications when the current date is 90, 60, and 30 days from an expiration date.
I've solved the problem (know basically what process is necessary) but am struggling to write the code. :/
Here's what I've pieced together from research. I think it's probably close, but I'm sure it's not correct.
Suggestions? Solutions?
function sendAlerts() {
var query = app.models.Customers.newQuery();
var today = new Date();
var expiresOn = new Date();
// query only for non-notified customers with upcoming due date
query.filters.NotificationAttempts._lessThan = ATTEMPTS_THRESHOLD;
query.filters.Notified._equals = false;
query.filters.DueDate._lessThan = expiresOn;
var customers = query.run();
customers.forEach(function(customer) {
var success = sendNotification(customer);
if (success) {
customer.Notified = true;
} else {
customer.NotificationAttempts++;
}
// performance trade off in favor of consistency
app.saveRecords([customer]);
});
function sendNotification_(to, subject, body)
{
MailApp.sendEmail({
to: 'beth#egaassociates.com',
subject: 'Expiration approaching',
body: 'You have a license, certification, or immunization expiring soon!',
noReply: true
});
}
}
/**
* Creates a time-driven trigger.
*/
function createTimeDrivenTriggers() {
// Trigger on set schedule.
ScriptApp.newTrigger('sendAlerts')
.timeBased()
.everyMinutes(2)
.create();
}
google.scripts.run.createTimeDrivenTriggers();

Disable multiple instances of onSubmit trigger during submissions

I have a form with onSubmit trigger, Which will be used as an domain wide add - on.The add - on can be used
for many forms.
I need to disable the multiple submit triggers, instead run only 1 instance currently.
Say the form submissions arrive at same time, I need to avoid the duplicate instances of the script running when the current script is running.
I tried to use lock service
function onFormSubmit(e) {
// lock the document response
var lock = LockService.getDocumentLock();
if (lock.hasLock()) return;
//do some tasks
lock.releaseLock();
}
How do I make a script (addon) to run as a single instance.
UPDATE: This code too, doesnt achive the intended functionality
function OnSubmits(e) {
var releaseLock;
releaseLock = function() {
lock.releaseLock();
}
var lock = LockService.getDocumentLock();
lock.tryLock(1);
if (!lock.hasLock()) {
return;
}
var value = cUseful.Utils.expBackoff(function() {
releaseLock();
return PropertiesService.getScriptProperties().getProperty('key');
});
if (value == null) value = 0;
else value++;
cUseful.Utils.expBackoff(function() {
var folder = DriveApp.getFolderById("1wFGMh38JGarJd8CaiaOynlB7iiL_Pw6D");
folder.addFile(DriveApp.getFileById(DocumentApp.create('Document Name ' + value).getId()));
PropertiesService.getScriptProperties().setProperty('key', value);
});
releaseLock();
}

concurrency issue and loosing form data in a google apps script powered google form

I've been recently having trouble with what I believe to be a concurrency issue when people are submitting the form near the same times, which is resulting in lost data for a google form. I'm already using the Lock service to prevent this issue, but I still seem to have problems. http://googleappsdeveloper.blogspot.com/2011/10/concurrency-and-google-apps-script.html
The form currently has onFormSubmit triggers: formSubmitReply and logMessage. formSubmitReply sends a confirmation to people that submitted the form and logMessage is supposed to back up the information in a separate spreadsheet in the case that rows in the regular spreadsheet get clobbered. It should be extracting the values from the formSubmit event and then appending it to the "log" sheet.
I've included all the current code for the script and replaced emails with place holders. Can I get some help identify anything buggy in the code that could be preventing the form from recording rows in the form?
function getColIndexbyName(colName){
var sheet=SpreadsheetApp.getActiveSheet();
var rowWidth=sheet.getLastColumn();
var row=sheet.getRange(1,1,1,rowWidth).getValues();//this is the first row
for ( i in row[0]){
var name=row[0][i];
if(name == colName || new RegExp(colName,'i').test(name)){
return parseInt(i)+1;
}
}
return -1
}
function makeReceipt(e){
/*This is for Student Volunteer auto-confirmation*/
var ss,sheet, rowWidth, headers, rowWidth,curRow, values, greeting, robot, msg, space, newline;
curRow=e.range.getRow();
ss=SpreadsheetApp.getActiveSpreadsheet();
sheet=ss.getSheetByName("RAW");
rowWidth=sheet.getLastColumn();
headers=sheet.getRange(1,1,1,rowWidth).getValues();
values=sheet.getRange(curRow,1,1,rowWidth).getValues();
greeting='Hi '+sheet.getRange(curRow,getColIndexbyName('First Name'),1,1).getValue()+"! <br><br>"+ ' ';
robot="<i>Below are the responses you submitted. Please let us know if any changes arise!</i> <br><br>";
msg=greeting+robot;
space=' ';
newline='<br>';
for( i in headers[0]){
//only write non "Reminders" column values
if(headers[0][i]!="Reminders"){
msg+="<b>";
msg+=headers[0][i];
msg+="</b>";
msg+=":";
msg+=space;
msg+=values[0][i];
msg+=newline;
}
}
return msg;
}
/**
* Triggered on form submit
**/
function formSubmitReply(e) {
var ss, row, mailIndex, userEmail, message, appreciation;
var lock = LockService.getPublicLock();
if(lock.tryLock(60000)){
try{
ss=SpreadsheetApp.getActiveSheet();
row=e.range.getRow();
mailIndex=getColIndexbyName('Email Address');
userEmail=e.values[mailIndex-1];
message=makeReceipt(e);
MailApp.sendEmail(userEmail, 'BP Day 2012 Confirmation for'+' '+userEmail,message,{name:"Name", htmlBody:message, replyTo:"example#example.com"});
messageAlert100(e);
} catch(err){
e.values.push("did not send email");
MailApp.sendEmail(""example#example.com","error in formSubmitReply"+err.message, err.message);
}
logToSpreadsheet(e);
} else {
//timeOut
try{
if(e && e.values){
logToSpreadsheet(e);
e.values.push("did not send email");
}
}catch(err){
MailApp.sendEmail("example#example.com", "error in logging script block "+err.message, err.message)
}
}
}
/**
* Triggered on form submit
**/
function messageAlert100(e){
var cheer_list, curRow, cheer_list, cheer_index, cheer, ss=SpreadsheetApp.getActiveSpreadsheet();
if(e && e.range.activate){
curRow=e.range.getRow();
}
cheer_list=["Congratulations!", "Give yourself a pat on the back!", "Yes!", "Cheers!","It's time to Celebrate!"];
cheer_index=Math.floor(Math.random()*cheer_list.length);
cheer=cheer_list[cheer_index];
if(typeof(curRow) != "undefined" && curRow % 100 ==0){
MailApp.sendEmail("example#example.com", ss.getName()+": "+cheer+" We now have "+ curRow + " Volunteers!", cheer+" We now have "+ curRow + " Volunteers!");
}
}
/**
*
**/
function logToSpreadsheet(e){
var ss=SpreadsheetApp.getActiveSpreadsheet(), sh;
if(!ss.getSheetByName("log")){
sh=ss.insertSheet("log");
}
sh=ss.getSheetByName("log");
if(e && e.values !==null){
sh.appendRow(e.values)
} else {
sh.appendRow(e);
}
Logger.log(e);
}
There is a very simple approach that I use to avoid concurrency issues with forms, I had to imagine that before GAS offered the lock method.
Instead of using the on form submit trigger I use a timer trigger (every few minutes or so) on a function that checks a column for a flag (MAIL SENT) ... if the flag is not present I send the email with processed data, copy to the backup sheet and set the flag. I do this on every row starting from the last one and stop when I find the flag. This way I'm sure all datarows are processed and no duplicate mail is sent.
It is actually very simple to implement, your script will need only a few modifications.
Viewed from the users side the result is nearly the same as they receive a mail just a few minutes after their submission.
EDIT : of course in this setup you cannot use e parameter to get the form data but you'll have to read data on the sheet instead... but that's not really a big difference ;-)