Related
I'm trying to create an app script sends an email when a new row is added into my Google sheet.
I have everything working for when I manually add in the data. However, when the data is auto added based on a form fill-- the email doesn't send.
I'm thinking that if I add a script that copies & pastes the email address after the new line is created, it will trigger the script to send an email. As of right now, auto-added rows do not send emails.
Thanks for the help!
Here's a link to my sheet.
Here's the App Script I'm using:
function sendEmailNotificationAndInsertTimestamp_(e) {
try {
if (!e) {
throw new Error('Please do not run the script in the script editor window but set up a trigger. See the instructions in the script.');
}
var range = e.range ? e.range : SpreadsheetApp.getActive().getActiveSheet().getActiveRange();
if (!range || JSON.stringify(e.range) === '{}') {
return;
}
var sheet = range.getSheet();
if (range.getA1Notation() === 'A1' && sheet.getName() === SpreadsheetApp.getActive().getSheets()[0].getName()) {
return;
}
var notificationSettings = [
{
sheetsToWatch: /^(EmailSender)$/i,
columnLabelsToWatch: ['Email'],
columnValuesToWatch: [undefined],
columnLabelsOfEmailAddresses: ['Send to'],
columnLabelsOfEmailSubjects: ['Subject'],
columnLabelsOfEmailContents: ['Body'],
columnLabelsToTimestamp: ['Completed Timestamp'],
insertTimestamps: [true],
overwriteTimestamps: [true],
eraseTimestamps: [false],
timestampFormat: 'm/d/yyyy h:mm am/pm',
columnLabelRow: sheet.getFrozenRows() || 1,
}
];
var rowStart = range.getRow();
var columnStart = range.getColumn();
var settings = getSettings_(sheet, notificationSettings);
if (!settings || rowStart <= settings.columnLabelRow) {
return;
}
var keysToRead = ['columnLabelsToWatch', 'columnLabelsOfEmailAddresses', 'columnLabelsOfEmailSubjects', 'columnLabelsOfEmailContents', 'columnLabelsToTimestamp'];
var columnLabelArrayLengths = ['columnValuesToWatch'].concat(keysToRead).map(getArrayLengths_(settings));
if (!isStrictlyEqual_.apply(null, columnLabelArrayLengths)) {
throw new Error('Mismatch between number of items in ' + keysToRead.join(', ') + '.');
}
var keysToWrite = ['columnsToWatch', 'columnsOfEmailAddresses', 'columnsOfEmailSubjects', 'columnsOfEmailContents', 'columnsToTimestamp'];
if (keysToRead.length !== keysToWrite.length) {
throw new Error('The number of items in keysToRead differs from the number of items in keysToWrite.');
}
var columnNumbers = getColumnNumbers_(sheet, settings, keysToRead, keysToWrite);
if (!columnNumbers) {
return;
}
var columnNumberArrayLengths = keysToWrite.map(getArrayLengths_(columnNumbers));
if (!isStrictlyEqual_.apply(null, columnNumberArrayLengths)) {
throw new Error('Could not find all the columns in ' + keysToRead.join(', ') + '.');
}
// iterate modified values
var now = new Date();
var emailAddressCheck = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var values = range.getDisplayValues();
var emailRecipients = [];
for (var column = 0, numColumns = values[0].length; column < numColumns; column++) {
var columnIndex = columnNumbers.columnsToWatch.indexOf(columnStart + column);
if (columnIndex === -1) {
continue;
}
var toWatch = settings.columnValuesToWatch[columnIndex];
var regex
= toWatch === undefined
? /./i
: typeof toWatch === 'string'
? new RegExp('^' + escapeRegExCharacters_(toWatch) + '$', 'i')
: toWatch instanceof RegExp
? toWatch
: null;
if (!regex) {
throw new Error('All columnValuesToWatch must be regular expressions or text strings, but "' + toWatch + '" is a ' + (typeof toWatch) + '.');
}
for (var row = 0, numRows = values.length; row < numRows; row++) {
var value = values[row][column];
if (value === '' && settings.columnValuesToWatch[columnIndex] === undefined) {
continue;
}
if (!value.match(regex)) {
continue;
}
var emailAddress = sheet.getRange(rowStart + row, columnNumbers.columnsOfEmailAddresses[columnIndex]).getDisplayValue().trim();
var emailSubject = sheet.getRange(rowStart + row, columnNumbers.columnsOfEmailSubjects[columnIndex]).getDisplayValue().trim();
var emailContents = sheet.getRange(rowStart + row, columnNumbers.columnsOfEmailContents[columnIndex]).getDisplayValue().trim();
if (emailAddress.split(',').some(address => !address.trim().replace(/^(.+<)|(>$)/g, '').match(emailAddressCheck))
|| [emailAddress, emailSubject, emailContents].some(string => string === '')) {
throw new Error('Cannot send email with these parameters: To: "' + emailAddress + '", Subject: "' + emailSubject + '", Content: "' + emailContents + '"');
}
// send email
MailApp.sendEmail(emailAddress, emailSubject, emailContents);
emailRecipients.push(emailAddress);
// insert timestamp
if (settings.insertTimestamps[columnIndex]) {
var timestampCell = sheet.getRange(rowStart + row, columnNumbers.columnsToTimestamp[columnIndex]);
var timestamp
= value.length
? now
: settings.eraseTimestamps[columnIndex]
? null
: now;
if (timestamp && !settings.overwriteTimestamps[columnIndex] && timestampCell.getValue()) {
continue;
}
timestampCell.setValue(timestamp).setNumberFormat(settings.timestampFormat);
}
} // row
} // column
if (emailRecipients.length) {
showMessage_('Sent email of the status change to ' + emailRecipients.filter(filterUniques_).join(', ') + '.');
}
} catch (error) {
showAndThrow_(error);
}
}
function getSettings_(sheet, settingsArray) {
// version 1.0, written by --Hyde, 26 February 2020
// - initial version
var settings = null;
var sheetName = sheet.getName();
for (var s = 0, numSettings = settingsArray.length; s < numSettings; s++) {
if (settingsArray[s].sheetsToWatch.test(sheetName)) {
settings = settingsArray[s];
break;
}
}
return settings;
}
function getColumnNumbers_(sheet, settings, keysToRead, keysToWrite) {
// version 1.0, written by --Hyde, 26 February 2020
// - initial version
var columnLabelRowValues = sheet.getRange(settings.columnLabelRow, /*column*/ 1, /*numRows*/ 1, sheet.getLastColumn()).getValues()[0];
var columnLabels = columnLabelRowValues.map(function stringTrim(value) {
return String(value).trim();
});
var arrays = {};
for (var k = 0, numKeys = keysToRead.length; k < numKeys; k++) {
var keyToRead = keysToRead[k];
var keyToWrite = keysToWrite[k];
arrays[keyToWrite] = [];
for (var i = 0, col, numColumns = settings[keyToRead].length; i < numColumns; i++) {
if ((col = columnLabels.indexOf(settings[keyToRead][i])) !== -1) {
arrays[keyToWrite].push(col + 1);
}
}
}
return arrays[keysToWrite[0]].length ? arrays : null;
}
function getArrayLengths_(obj) {
// version 1.0, written by --Hyde 28 February 2020
// - initial version
var closure = function(key) {
return obj[key].length;
};
return closure;
}
function isStrictlyEqual_(value1, value2) {
// version 1.0, written by --Hyde 28 February 2020
// - initial version
var args = Array.prototype.slice.call(arguments);
var numArgs = args.length;
if (!numArgs) {
return null;
}
var checkAgainst = args[0];
for (var a = (1); a < numArgs; a++) {
if (args[a] !== checkAgainst) {
return false;
}
}
return true;
}
function filterUniques_(element, index, array) {
// version 1.0, written by --Hyde, 30 May 2019
// - initial version
return array.indexOf(element) === index;
}
function escapeRegExCharacters_(string) {
// version 1.1, written by --Hyde, 9 November 2018
// - use String
// version 1.0, written by --Hyde, 30 November 2018
// - initial version
// - adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function showAndThrow_(error) {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
var stackCodeLines = String(error.stack).match(/\d+:/);
if (stackCodeLines) {
var codeLine = stackCodeLines.join(', ').slice(0, -1);
} else {
codeLine = error.stack;
}
showMessage_(error.message + ' Code line: ' + codeLine, 30);
throw error;
}
function showMessage_(message, timeoutSeconds) {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
SpreadsheetApp.getActive().toast(message, 'Send email notification script', timeoutSeconds || 5);
}
function installOnEditTrigger() {
// version 1.0, written by --Hyde, 9 January 2020
// - initial version
deleteTriggers_(ScriptApp.EventType.ON_EDIT);
deleteTriggers_(ScriptApp.EventType.ON_CHANGE);
ScriptApp.newTrigger('sendEmailNotificationAndInsertTimestamp_')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
function installOnFormSubmitTrigger() {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
deleteTriggers_(ScriptApp.EventType.ON_FORM_SUBMIT);
ScriptApp.newTrigger('sendEmailNotificationAndInsertTimestamp_')
.forSpreadsheet(SpreadsheetApp.getActive())
.onFormSubmit()
.create();
}
function installOnChangeTrigger() {
// version 1.0, written by --Hyde, 7 May 2020
// - initial version
deleteTriggers_(ScriptApp.EventType.ON_EDIT);
deleteTriggers_(ScriptApp.EventType.ON_CHANGE);
ScriptApp.newTrigger('sendEmailNotificationAndInsertTimestamp_')
.forSpreadsheet(SpreadsheetApp.getActive())
.onChange()
.create();
}
function deleteTriggers_(triggerType) {
// version 1.0, written by --Hyde, 7 May 2020
// - initial version
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0, numTriggers = triggers.length; i < numTriggers; i++) {
if (triggers[i].getEventType() === triggerType) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
}
I am trying to plot the following sample data using a LineChart with a CategoryFilter to filter on year.
Data table is defined as:
aggData: [Date: date][Team: string][Score: number]
From the aggData table I dynamically calculate the default hAxis ticks as
var hAxisTicks = [];
var dateRange = aggData.getColumnRange(0);
for (var date = dateRange.min; date <= dateRange.max; date = new Date(date.getFullYear(), date.getMonth() + 1)) {
hAxisTicks.push(date);
}
The year picker and the line chart are configured as:
var yearPicker = new google.visualization.ControlWrapper({
controlType: 'CategoryFilter',
containerId: 'categoryFilter_div',
options: {
filterColumnIndex: 0,
ui: {
allowTyping: false,
allowMultiple: false,
label: 'Year:',
labelStacking: 'vertical'
},
useFormattedValue: true
}
});
var lineChart = new google.visualization.ChartWrapper({
chartType: 'LineChart',
containerId: 'chart_div',
options: {
width: 900,
height: 500,
hAxis: {
format: 'MMM', ticks: hAxisTicks
}
}
});
I added the following event listener
google.visualization.events.addListener(yearPicker, 'statechange', function () {
google.visualization.events.addOneTimeListener(lineChart, 'ready', getTicks);
});
I need to create/recreate the hAxis ticks everytime the yearPicker changes by calling getTicks
function getTicks() {
var ticks = [];
if (yearPicker.getState().selectedValues.length > 0) {
for (var i = 0; i <= hAxisTicks.length; i = i + 1) {
var date = new Date(hAxisTicks[i]);
if (date.getFullYear() == yearPicker.getState().selectedValues[0]) {
ticks.push(date)
}
}
}
else {
for (var i = 0; i <= hAxisTicks.length; i = i + 1) {
var date = new Date(hAxisTicks[i]);
ticks.push(date);
}
lineChart.setOption('hAxis.ticks', ticks);
lineChart.draw();
}
lineChart.setOption('hAxis.ticks', ticks);
lineChart.draw();
}
Here's what happens at different stages
1- When page first loads the graph looks like (the getTicks function is NOT called) which is correct:
2- When the year is changed to 2019, for example, the hAxis ticks get recalculated (the getTicks function is does get called) and the graph appears to be correct
3- Attempting to go back to the default chart to display all years, an a.getTime is not a function error message appears under the CategoryFilter
4- Any subsequent attempts to change the CategoryFilter to any value throws a ```One or more participants failed to draw()
How can I rectify this behavior?
I realized my iteration over the hAxisTics array was incorrect. I should stop at < hAxisTics.length instead of <= hAxisTics.length and I should recalculate inside the event handler
google.visualization.events.addListener(yearPicker, 'statechange', function () {
var ticks = [];
if (yearPicker.getState().selectedValues.length > 0) {
for (var i = 0; i < hAxisTicks.length; i = i + 1) {
var date = new Date(hAxisTicks[i]);
if (date.getFullYear() == yearPicker.getState().selectedValues[0]) {
ticks.push(date)
}
}
}
else {
for (var i = 0; i < hAxisTicks.length; i = i + 1) {
var date = new Date(hAxisTicks[i]);
ticks.push(date);
}
lineChart.setOption('hAxis.ticks', ticks);
lineChart.draw();
}
lineChart.setOption('hAxis.ticks', ticks);
lineChart.draw();
});
The task is a booking form for a restaurant. The disabled time is the same in every Restaurant except in one of them. All Restaurants can be chosen via select options. The select field already has a js function called show for different functions.
I have created js to set the time between 17 and and 23 to be disabled when the Restaurant has been Picked beforehand.
However I cannot get the picker to reload on change. I've tried it with function show() and $('#Restaurant').change(function(). I realize I am not telling the picker to change the values, but I'm not sure how.
<select name="Restaurant" id="Restaurant" onchange="show()" required data-msg-required="Please choose the restaurant">
<?php if (empty($_GET['Restaurant'])){echo '<option disabled selected
value="">Choose restaurant</option>';}?>
<?php booking(); ?>
</select>
<script>
$(function () {
$('#DatumZeit').datetimepicker({
locale: 'en',
minDate: moment().add(2, 'hours'),
disabledHours: disabledtime,
format: 'DD.MM.YYYY HH:mm',
showClose: true,
icons: {
close: 'OK'
},
widgetPositioning: {
horizontal: 'auto',
vertical: 'bottom'
},
});
});
function show(){
if(document.getElementById('Restaurant').value == "Düsseldorf") {
var currenttime = moment();
var timestart = moment().startOf('day').hour(10).add(7, 'hours');
var timeend = moment().startOf('day').hour(10).add(13, 'hours');
console.log(timestart);
console.log(timeend);
if (currenttime.isBetween(timestart, timeend)) {
console.log('is between');
var disabledtime= [10,9,8,7,6,5,4,3,2,1,00,24,23,17,18,19,20,21,22];
} else {
console.log('is not between');
var disabledtime= [10,9,8,7,6,5,4,3,2,1,00,24,23];
}
};
}
</script>
Maybe it can help someone else:
Basically I used $('#DatumZeit').data("DateTimePicker").disabledHours.
function show(){
if(document.getElementById('Restaurant').value == "Düsseldorf") {
var currenttime = moment();
var timestart = moment().startOf('day').hour(10).add(7, 'hours');
var timeend = moment().startOf('day').hour(10).add(13, 'hours');
if (currenttime.isBetween(timestart, timeend)) {
$('#DatumZeit').data("DateTimePicker").disabledHours([
10,9,8,7,6,5,4,3,2,1,00,24,23,17,18,19,20,21,22
]);
console.log('is between');
} else {
$('#DatumZeit').data("DateTimePicker").disabledHours([
10,9,8,7,6,5,4,3,2,1,00,24,23
]);
console.log('is not between');
}
};
if(document.getElementById('Restaurant').value != "Düsseldorf") {
$('#DatumZeit').data("DateTimePicker").disabledHours([
10,9,8,7,6,5,4,3,2,1,00,24,23
]);
}
}
I actually have a better version for my own needs that might work for other people as well.
function show(){
if(document.getElementById('Restaurant').value == "Düsseldorf") {
var myDate = new Date();
var currenttime = moment(myDate);
var timestart = moment().startOf('day').hour(17);
var timeend = moment().startOf('day').hour(23);
if (currenttime.isBetween(timestart, timeend)) {
$('#DatumZeit').data("DateTimePicker").date(moment().add(1, 'days')).disabledHours(
[0,1,2,3,4,5,6,7,8,9,10,23,24]
)
}
else {
$('#DatumZeit').data("DateTimePicker").date(moment(myDate)).disabledHours(
[0,1,2,3,4,5,6,7,8,9,10,23,24]
)
}
} else {
$('#DatumZeit').data("DateTimePicker").date(moment().add(2, 'hours')).enabledHours(
[11, 12 ,13 ,14 ,15, 17,16, 18, 19, 20, 21, 22]
)
}
}
I try to disabled the date for holidays and weekend but that not work
My code :
$(function() {
$('.thing12DatePicker').datepicker(
{
beforeShowDay: function (date) {
var startDate = "2017-06-17",
endDate = "2017-06-21",
dateRange = [];
for (var d = new Date(startDate); d <= new Date(endDate); d.setDate(d.getDate() + 1)) {
dateRange.push($.datepicker.formatDate('yy-mm-dd', d)&& $.datepicker.noWeekends);
}
var dateString = jQuery.datepicker.formatDate('yy-mm-dd', date);
return [dateRange.indexOf(dateString) == -1];
}
});
});
The date holidays are disabled but not the weekend !
I want disbled the weekend .
thx
I have found solution
var startDate = "2017-06-17",
endDate = "2017-06-21",
bankHoliDays = [];
for (var d = new Date(startDate); d <= new Date(endDate); d.setDate(d.getDate() + 1)){
bankHoliDays.push($.datepicker.formatDate('yy-mm-dd', d));
}
function disableDates(date) {
var dt = $.datepicker.formatDate('yy-mm-dd', date);
var noWeekend = jQuery.datepicker.noWeekends(date);
return noWeekend[0] ? (($.inArray(dt, bankHoliDays) < 0) ? [true] : [false]) : noWeekend;
}
$(function () {
$('#datepicker').datepicker({
beforeShowDay: disableDates
});
});
I have a couple of forms on a site I'm working on and the script that controls them doesn't include a success message, so when they're submitted the input data just disappears and the user doesn't know if it's been actually sent or not. I've looked around a bit for answers, but because this file controls an email submission form, a contact form, and a twitter feed, it's a bit much for me to see what's what.
Here's the code, I'd just like to let users know that their message has been sent for both the email input form and the contact form. I appreciate any help that's out there!
$(document).ready(function() {
//Set default hint if nothing is entered
setHints();
//Bind JavaScript event on SignUp Button
$('#signUp').click(function(){
signUp($('#subscribe').val());
});
//Bind JavaScript event on Send Message Button
$('#sendMessage').click(function(){
if(validateInput()){
sendMail();
}else
{
alert('Please fill all fields to send us message.');
}
});
//Load initial site state (countdown, twitts)
initialize();
});
var setHints = function()
{
$('#subscribe').attachHint('Enter your email to be notified when more info is available');
$('[name=contact_name]').attachHint('Name');
$('[name=contact_email]').attachHint('Email');
$('[name=contact_subject]').attachHint('Subject');
$('[name=contact_message]').attachHint('Message');
};
var signUp = function(inputEmail)
{
var isValid = true;
var emailReg = /^([\w-\.]+#([\w-]+\.)+[\w-]{2,4})?$/;
if(!emailReg.test(inputEmail)){
isValid = false;
alert('Your email is not in valid format');
}
if(isValid){
var params = {
'action' : 'SingUp',
'email' : inputEmail
};
$.ajax({
type: "POST",
url: "php/mainHandler.php",
data: params,
success: function(response){
if(response){
var responseObj = jQuery.parseJSON(response);
if(responseObj.ResponseData)
{
$('#subscribe').val('');
}
}
}
});
}
};
var initialize = function()
{
var params = {
'action' : 'Initialize'
};
$.ajax({
type: "POST",
url: "php/mainHandler.php",
data: params,
success: function(response){
if(response){
var responseObj = jQuery.parseJSON(response);
if(responseObj.ResponseData)
{
$('ul.twitts').empty();
if(responseObj.ResponseData.Twitts){
$('a.followUsURL').attr('href','http://twitter.com/#!/'+responseObj.ResponseData.Twitts[0].Name);
$.each(responseObj.ResponseData.Twitts, function(index, twitt){
var twitterTemplate = '<li>'
+ '#{0}'
+ '{2}'
+ '<span class="time">{3}</span>'
+ '</li>';
$('ul.twitts').append(StringFormat(twitterTemplate, twitt.Name, twitt.StatusID, twitt.Text, twitt.Date));
});
}
if(responseObj.ResponseData.Start_Date)
{
setInterval(function(){
var countDownObj = calculateTimeDifference(responseObj.ResponseData.Start_Date);
if(countDownObj){
$('#days').text(countDownObj.Days);
$('#hours').text(countDownObj.Hours);
$('#minutes').text(countDownObj.Minutes);
$('#seconds').text(countDownObj.Seconds);
}
}, 1000);
}
}
}
}
});
};
var validateInput = function(){
var isValid = true;
$('input, textarea').each(function(){
if($(this).hasClass('required'))
{
if($(this).val()!=''){
if($(this).hasClass('email'))
{
var emailReg = /^([\w-\.]+#([\w-]+\.)+[\w-]{2,4})?$/;
if(!emailReg.test($(this).val())){
isValid = false;
alert('Your email is not in valid format');
}
}
}else
{
isValid = false;
}
}
});
return isValid;
};
var resetInput = function(){
$('input, textarea').each(function() {
$(this).val('').text('');
});
};
var sendMail = function(){
var params = {
'action' : 'SendMessage',
'name' : $('[name=contact_name]').val(),
'email' : $('[name=contact_email]').val(),
'subject' : $('[name=contact_subject]').val(),
'message' : $('[name=contact_message]').val()
};
$.ajax({
type: "POST",
url: "php/mainHandler.php",
data: params,
success: function(response){
if(response){
var responseObj = jQuery.parseJSON(response);
if(responseObj.ResponseData)
$('label.sendingStatus').text(responseObj.ResponseData);
}
resetInput();
$('#sendMail').removeAttr('disabled');
},
error: function (xhr, ajaxOptions, thrownError){
//xhr.status : 404, 303, 501...
var error = null;
switch(xhr.status)
{
case "301":
error = "Redirection Error!";
break;
case "307":
error = "Error, temporary server redirection!";
break;
case "400":
error = "Bad request!";
break;
case "404":
error = "Page not found!";
break;
case "500":
error = "Server is currently unavailable!";
break;
default:
error ="Unespected error, please try again later.";
}
if(error){
$('label.sendingStatus').text(error);
}
}
});
};
var calculateTimeDifference = function(startDate) {
var second = 1000;
var minute = second * 60;
var hour = minute * 60;
var day = hour * 24;
var seconds = 0;
var minutes = 0;
var hours = 0;
var days = 0;
var currentDate = new Date();
startDate = new Date(startDate);
var timeCounter = startDate - currentDate;
if (isNaN(timeCounter))
{
return NaN;
}
else
{
days = Math.floor(timeCounter / day);
timeCounter = timeCounter % day;
hours = Math.floor(timeCounter / hour);
timeCounter = timeCounter % hour;
minutes = Math.floor(timeCounter / minute);
timeCounter = timeCounter % minute;
seconds = Math.floor(timeCounter / second);
}
var tDiffObj = {
"Days" : days,
"Hours" : hours,
"Minutes" : minutes,
"Seconds" : seconds
};
return tDiffObj;
};
var StringFormat = function() {
var s = arguments[0];
for (var i = 0; i < arguments.length - 1; i++) {
var regExpression = new RegExp("\\{" + i + "\\}", "gm");
s = s.replace(regExpression, arguments[i + 1]);
}
return s;
}
You need to hook into the success callbacks of each of the $.ajax calls. You can create a method that will show a message for those:
For example, your signUp function's success callback could look like:
success: function(response){
if(response){
var responseObj = jQuery.parseJSON(response);
if(responseObj.ResponseData)
{
$('#subscribe').val('');
showMessage('Your subscription was received. Thank you!');
}
}
}
And you just create a method that will show the message to the user
var showMessage = function (msg) {
// of course, you wouldn't use alert,
// but would inject the message into the dom somewhere
alert(msg);
}
You would call showMessage anywhere the success callback was fired.
You can add your success notifing code in each of the $.ajax success handlers.