Form submission handler creates duplicate Google Calendar events - google-apps-script

I modified code from this question to suit my needs. I use Google Forms that fill into Google Sheets, and then the script inserts an event into Google Calendar. I have installed a "form submit" trigger to execute the script. The issue I have is that my code starts with the data in the first row of the sheet, adding the same events every time a new form response is added.
Here is my version:
//push new events to calendar
function pushToCalendar() {
//spreadsheet variables
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = sheet.getLastRow();
var range = sheet.getRange(2,1,lastRow,16);
var values = range.getValues();
//calendar variables
var defaultCalendar = CalendarApp.getDefaultCalendar()
var numValues = 0;
for (var i = 0; i < values.length; i++) {
//check to see if Start DateTime and End DateTime are filled out
if ((values[i][3]) && (values[i][4])) {
//check if it's been entered before
if (values[i][6] != 'y') {
//create event https://developers.google.com/apps-script/class_calendarapp#createEvent
var newEventTitle = values[i][1] + ' - ' + values[i][2];
var startDay = Utilities.formatDate(new Date(values[i][3]), "GMT", "yyyy-MM-dd'T'HH:mm:ss'Z'");
var endDay = Utilities.formatDate(new Date(values[i][4]), "GMT", "yyyy-MM-dd'T'HH:mm:ss'Z'");
var newEvent = defaultCalendar.createEvent(newEventTitle, new Date(startDay), new Date(endDay), {location: values[i][2]});
//get ID
var newEventId = newEvent.getId();
//mark as entered, enter ID
sheet.getRange(i + 2, 6).setValue('y');
sheet.getRange(i + 2, 7).setValue(newEventId);
}
}
numValues++;
}
}

Found the solution by myself: on row 20 i changed the code
if (values[i][6] != 'y') {
to
if (!values[i][6]) {
No need to write that 'y' sign. I also changed
sheet.getRange(i+2,6).setValue('y');
to
sheet.getRange(i+2,6).setValue('');
And no duplicates anymore. The code works perfect!

The issue with the original code is that you are reading the wrong column - JavaScript arrays are 0-base, while the spreadsheet columns are 1-base. Column 6 (from getRange(i + 2, 6) is in the array index 5, i.e. values[i][5], not values[i][6]. So you were comparing the newEventId with the string 'y', which was never going to be the same.
As your solution indicates, removing the condition that values[i][6] (aka column 7, where your script writes the created event's ID) is not equal to 'y' (a condition that was always true), and instead testing for any value will appropriately guard the event creation code. Given the presence of the event ID column, the column in which 'y' was written is entirely unnecessary.
If you remove that column from your form response sheet, the code guard would then be:
//check to see if Start DateTime and End DateTime are filled out
if ((values[i][3]) && (values[i][4])) {
//check if it's been entered before, by looking in Column F:
var existingEventId = values[i][5]; // Will be "" (falsy) if not yet added
if (!existingEventId) {
...
// Log the event's ID so we don't make a duplicate:
sheet.getRange(i + 2, 6).setValue(newEventId);
...

Related

Prevent CalendarApp from creating duplicates

---EDITED---
I think I found a way to solve this without saving IDs anywhere.
Thanks for your replies. Full code below:
// -------------- EDIT CODE BELOW FOR NEW CLIENT ------------
//EDIT CODE IN BETWEEN THESE LINES ------------------- ( more code to be edited below) //
var client = "CLIENTNAME";
var email = "EMAIL";
var calId = "c_7esq0q0d1n06ao5bc7ueqt7dp8#group.calendar.google.com";
//EDIT CODE IN BETWEEN THESE LINES ------------------- ( more code to be edited below)
//Common data for all triggers
// getting data from spreadsheet
var ss = SpreadsheetApp.getActive();
var ssurl = s.getUrl();
var sheet = SpreadsheetApp.getActive().getSheetByName('Projects');
var startRow = 2; // First row of data to process
var numRows = sheet.getLastRow(); //get last row with data
var numCols = sheet.getLastColumn();
var dataRange = sheet.getRange(startRow, 1, numRows, numCols);
var data = dataRange.getValues();
// Triggers below -- Triggers below -- Triggers below -- Triggers below
function AddProjDeadlinesToCalendar() {
//RFQ event creator
//looping through all of the rows
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var rfqColumn = row[10]; // ENTER THE COLUMN NUMBER, STARTING FROM 0, FOR THIS DEADLINE
var rfqTitle = row[0] + ' ready for RFQ.'; // ENTER THE TITLE OF THE EVENT
var rfqDescript = row[0] + ' design files should be ready for RFQs by this date.'; // ENTER THE DESCRIPTION OF THE EVENT
var rfqExpireDateDay = new Date(rfqColumn).getDate();
Logger.log(rfqColumn);
//-------------- Add event to Calendar ----------
if (!isNaN(parseFloat(rfqExpireDateDay)) && isFinite(rfqExpireDateDay)){
// Determines how many events are present in this calendar, from now till the next 365 days, that contains a keyword
var now = new Date();
var inAYear = new Date(now.getTime() + (365 * 24 * 60 * 60 * 1000));
var events = CalendarApp.getCalendarById(calId).getEvents(now, inAYear,
{search: rfqTitle});
Logger.log('Events with same title already present: ' + events.length);
// If there is more than 0, get event's ID and do not create this event.
if (events.length > 0){
for (i=0; i<events.length; i+1){
var eventIDold = events[i];
Logger.log('Event ID already present : ' + eventIDold.getId());
return;
}
}else{
//Creates an all-day event and logs the ID.
var rfqEvent = CalendarApp
.getCalendarById(calId)
.createAllDayEvent(rfqTitle, new Date(rfqColumn),
{description: rfqDescript});
Logger.log('Newly created Event ID: ' + rfqEvent.getId());
}
}
}
}
//-------------- Add event to Calendar ---------- ENDS HERE
It works, but it stops at the first event that is already present on the calendar (return) and will not check for all other rows in the column's array.
Both break and continue lead to an endless loop. My basic knowledge of coding doesn't let me see the problem. I bet it's in plain sight.
-----END EDIT----
I'm need of help for my script that feature the CalendarApp.
I am already using the script below to create new events with a template for the description and title.
---------- Add event to Calendar ----------
if (!isNaN(parseFloat(expireDateDay)) && isFinite(expireDateDay)){
//Creates an all-day event and logs the ID.
var event = CalendarApp
.getCalendarById('calendarIDhere')
.createAllDayEvent(row[0] + ' ready for RFQ.', new Date(rfqColumn),
{description: row[0] + ' exampletexthere'});
Logger.log('Event ID: ' + event.getId());
}
//-------------- Add event to Calendar ----------
The script gets some data from a previously created array (row[0]) and gets the event's date from, again, a previous variable (rfqColumn).
The main script loops through all rows in a specific column (rfqColumn) and creates events for each date it finds in that column. Title and description are gathered from the same row as where the date is found.
I have ready many threads about the CalendarApp and how to prevent duplicates. None of them use this feature as I do.
If possible, I would like to avoid having a sheet with all the calendar's events IDs.
Is it possible to:
Every time a new event is created, check if there's already an event with same title and update it if so?
Thanks in advance to anyone who will spend a minute sharing his expertise.
Solved. Code edit below in case it could help someone.
In my specific case, there could be only one instance with same title, so the for loop can end after first occurrence. (j<1).
if (events.length > 0){
for (j=0; j<1; j++){
var eventIDold = events[j];
Logger.log('Event ID already present : ' + eventIDold.getId());
}

Delete Cells Based on Date

I need a help with a cell-deletion script. In general, I want to run a reset script that clears out all of the data up to the day I run it. Because I am statically inputting values into those cells that are matching up with information from a filter, I believe I need to delete those cells to properly line up my inputs with where the filter information will be after I delete the expired rows from the exporting page.
Here's what I want to do in my script: If the Column F value < today's date, then delete the cells in I, J, and K and shift the cells below them up. I think I found code to do this, but it takes so long to run that the program times out before it can get through more than a few rows. I will use a for loop to run it over 73 pages, so if it is lagging out on one...yeah, I need help!
function deleteEntries() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var datarange = ss.getDataRange();
var lastrow = datarange.getLastRow();
var values = datarange.getValues();
var currentDate = new Date();
for (i = lastrow; i >= 5; i--) {
var tempdate = values[i-1][5];
if (tempdate < currentDate)
{
ss.getRange(i-1, 8).deleteCells(SpreadsheetApp.Dimension.ROWS);
ss.getRange(i-1, 9).deleteCells(SpreadsheetApp.Dimension.ROWS);
ss.getRange(i-1, 10).deleteCells(SpreadsheetApp.Dimension.ROWS);
}}}
In accordance with Apps Script "best practices", you will want to limit the use of the Spreadsheet Service to improve execution times. There are two "immediate" optimizations that can be considered:
Delete more than 1 cell at a time in a row
To do this, simply select a 1-row x 3-column range: ss.getRange(i-1, 8, 1, 3) instead of selecting (i-1, 8), (i-1, 9), (i-1, 10) and calling deleteCells on each of the three Ranges.
Sort your sheet before deleting such that only 1 delete call is necessary (e.g. the C++ stdlib "erase-remove" idiom). If your data is sorted based on column F, such that all data that should be removed is at the end, then you simply need to iterate the in-memory array (a very fast process) to locate the first date that should be removed, and then remove all the data below & including it.
An implementation of option 2 would look like this (I assume you use frozen headers, as they do not move when the sheet or range is sorted).
function sortDescAndGetValuesBack_(s, col) {
return s.getDataRange().sort({column: col, ascending: false}).getValues();
}
function deleteAllOldData() {
const sheets = SpreadsheetApp.getActive().getSheets()
.filter(function (sheet) { /** some logic to remove sheets that this shouldn't happen on */});
const now = new Date();
const dim = SpreadsheetApp.Dimension.ROWS;
sheets.forEach(function (sheet) {
var values = sortDescAndGetValuesBack_(sheet, 6); // Col 6 = Column F
for (var i = sheet.getFrozenRows(), len = values.length; i < len; ++i) {
var fVal = values[i][5]; // Array index 5 = Column 6
if (fVal && fVal < now) { // if equality checked, .getTime() is needed
console.log({message: "Found first Col F value less than current time",
index: i, num2del: len - i, firstDelRow: values[i],
currentTime: now, sheet: sheet.getName()});
var delRange = sheet.getRange(1 + i, 8, sheet.getLastRow() - i, 3);
console.log({message: "Deleting range '" + sheet.getName() + "!" + delRange.getA1Notation() + "'"});
delRange.deleteCells(dim);
break; // nothing left to do on this sheet.
}
}
console.log("Processed sheet '" + sheet.getName() + "'");
});
}
References:
Array#filter
Array#forEach
Range#sort
Range#deleteCells

Google Apps Script: how to copy paste ranges based on formulas?

I have a model in Google Sheets that is set up with one column per day. It contains both actuals and forecasts, and every day I need to roll forward formulas to replace forecasts with actuals. I can't roll forward the whole column, only a segment of it (there are reference numbers above and below that shouldn't be changed).
I have tried to write a script to do this for me every day, but I don't know how to make getRange reference a dynamic range. This is my attempt:
function rollColumn() {
var ss2 = SpreadsheetApp.openById('<ID redacted>');
ss2.getRange("=index(Model!$7:$7,,match(today()-2,Model!$4:$4,0)):index(Model!$168:$168,,match(today()-2,Model!$4:$4,0))").copyTo(ss2.getRange("=index(Model!$7:$7,,match(today()-1,Model!$4:$4,0)):index(Model!$168:$168,,match(today()-1,Model!$4:$4,0))"))
};
The INDEX formulas work insofar as they reference the relevant ranges (I have tested them in the spreadsheet). But clearly getRange doesn't accept formulas as an input. It also seems that Google Sheets doesn't allow for a named range to be created with formulas (which is how I would solve this in Excel).
Can someone help me recreate this functionality with GAS?
This is the closest existing question I've found on Stack Overflow, but I haven't been able to make it work:
Google Apps Script performing Index & Match function between two separate Google Sheets
Thank you!
You should add {contentsOnly:false} parameter to your code. something like this:
TemplateSheet.getRange("S2:T2").copyTo(DestSheet.getRange("S2:T"+LRow2+""), {contentsOnly:false});
Getting a date from column's title, then pasting formulas to the row to the right:
// note: we assume that sheet is disposed as in the following document: https://docs.google.com/spreadsheets/d/1BU2rhAZGOLYgzgSAdEz4fJkxEcPRpwl_TZ1SR5F0y08/edit?ts=5a32fcc5#gid=0
function find_3formulas() {
var sheet = SpreadsheetApp.getActiveSheet(),
leftTitle, // this variable will stay unused because we do not need a vertical index
topTitle = todayMinus_xDays(2),
topTitlesRange = sheet.getRange("G3:T3"),
leftTitlesRange = sheet.getRange("A4:A8"); // this range will stay unused.
var coor = findCoordinates(leftTitlesRange, leftTitle, topTitlesRange, topTitle);
if (coor.row == null || coor.column == null) {
sheet.getRange("M12:M14").setFormula('="NULL: please check logs"');
return;
}
var rowAxis = 4 + coor.row;
var colAxis = 8 + coor.column;
var fromRange = sheet.getRange(rowAxis, colAxis, 3, 1);
var toRange = sheet.getRange(rowAxis, colAxis + 1, 3, 1);
Logger.log(fromRange.getA1Notation())
Logger.log(toRange.getA1Notation());
var threeFormulas = fromRange.getFormulas();
toRange.setFormulas(threeFormulas)
}
// unused in current script!
function findCoordinates(leftTitlesRange, leftTitle, topTitlesRange, topTitle) {
var formattedDate,
row = 0,
column = 0;
if (leftTitle) {
row = findRow(leftTitlesRange, leftTitle);
}
if (topTitle) {
column = findColumn(topTitlesRange, topTitle);
}
var array = {row:row, column:column}
return array;
}
// unused in current script!
function findRow(range, valueToSearch) {
var colRows = range.getValues();
for (i = 0; i < colRows.length; i++) {
if (valueToSearch == colRows[i][0]) {return i;}
}
// however, if found nothing:
Logger.log("the value " + valueToSearch + " could not be found in row titles");
return null;
}
// assumes that column titles are dates, therefore of type object.
function findColumn(range, valueToSearch) {
var colTitles = range.getValues();
for (i = 0; i < colTitles[0].length; i++) {
if (typeof colTitles[0][i] == "object") {
formattedDate = Utilities.formatDate(colTitles[0][i], "GMT", "yyyy-MM-dd")
};
if (valueToSearch === formattedDate) {return i;}
}
// however, if found nothing:
Logger.log("today's date, " + valueToSearch + ", could not be found in column titles");
return null;
}
// substracts 2 days from today, then returns the result in string format.
function todayMinus_xDays(x) {
var d = new Date();
d = new Date(d - x * 24 * 60 * 60 * 1000);
d = Utilities.formatDate(d, "GMT", "yyyy-MM-dd");
return d;
}

Cannot find function getMonth in object 33463

I have created a script to send emails to a specific people with Birthday Reminder. This use to work till day before yesterday. I don't know why am I getting this error that Cannot find function getMonth, Can anyone tell where is the mistake
function emailAlert() {
// Short circuit if email notice is set to "No". This causes an error and the script stops.
if (turnOnEmailNotice.toLowerCase() == "no")
{
Logger.log("The Email Notification is NOT turned ON. System will Exit.");
exit
}
//Get the total number of filled row in the sheet.
var currentRowAT = 2;
var currentCellValueAT = "start";
while (currentCellValueAT != ""){
if (currentCellValueAT = birthdaysSheet.getRange("G" + currentRowAT).getValue() != ""){
currentRowAT = currentRowAT +1;
}
}
var birthdaysSheetLastRow = currentRowAT - 1;
// Get today's Date (with Month, Date and Year)
var today = new Date();
var todayMth = today.getMonth()+1;
var todayDate = today.getDate();
var todayYear = today.getFullYear();
// Check sheet cell for match to alertDate, k is the current row number being checked. Starting at 2 as the row #1 is for the headers
for (k=2; k < birthdaysSheetLastRow + 1; k++)
{
var targetBday = new Date();
targetBday = birthdaysSheet.getRange("P" + k).getValue();
// If Birthday is not speicified, continue with the next row
if (targetBday == ""){continue};
var unadjTargetBday = new Date();
var unadjTargetBdayMth = targetBday.getMonth()+1;
var unadjTargetBdayDate = targetBday.getDate();
var unadjTargetBdayYear = targetBday.getFullYear();
var unadjTargetBday = targetBday;
targetBday.setDate(targetBday.getDate()-daysInAdvance); // Calculating how many days in advance you want to trigger the notification. This is set in Settings Tab.
var targetBdayMth = targetBday.getMonth()+1;
var targetBdayDate = targetBday.getDate();
if (targetBdayMth + " " + targetBdayDate == todayMth + " " + todayDate)
{
var targetBirthDateYearsOld = (today.getYear() - unadjTargetBday.getYear())-1900;
prepareAndSendEmail(k, targetBirthDateYearsOld);
}
}
}
getValue will return the type string/date/number depending on the cell type in the spreadsheet (see menu Format -> Number). To be sure, just always convert to Date Type. This is the right way to convert it:
var targetBday = new Date(birthdaysSheet.getRange("P" + k).getValue());
Short answer
Check that the cell value is a valid date for Google Sheet.
Explanation
From https://developers.google.com/apps-script/reference/spreadsheet/range#getvalue (emphasis mine)
getValue()
Returns the value of the top-left cell in the range. The value may be
of type Number, Boolean, Date, or String depending on the value of the
cell. Empty cells will return an empty string.
To avoid this kind of problems, you may include some sort of data validation. You could use build-in features like conditional formatting, data validation, or something like a on edit script together with try / catch or a redundant validation in your emailAlert script.

Google Apps Script- Increment column each day- Google Spreadsheet

I need the apps script to scan each column each day on incremental basis. If it scans one column today say A column, next day the next column B should be scanned.
To trigger the function, daily timer is used for a specific time in a day.
The below code scans all the columns at a time.
The problem here is, the apps script is triggered daily one time. So it starts from beginning. How to store which column it had scanned yesterday and increment it for today.
function AutoSend() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var list= new Array();
for(var j= 2; j<=29;j++){ // for Column
for(var i=5;i<=23;i++){ // for row
var value = sheet.getRange(i, j).getValue();
if(value !== "OFF"){ if(value !== "L") { if(value !== "COMP OFF") {
var EmpName= sheet.getRange(i,1).getValue();
list.push(EmpName);
}}}
}
}
Logger.log(list);
var subject= "sub";
var message = list;
MailApp.sendEmail('example#gmail.com',subject, message);
}
You'll probably need to store data in the document properties.
PropertiesService.getDocumentProperties().setProperty(key, value);
You may want to use the date as a string for the key, and the column number as the value.
var theColumn = 9; //To Do - get the correct column number
var todaysDate = new Date().toString();
Logger.log('todaysDate: ' + todaysDate);
todaysDate = todaysDate.slice(0,15); //Remove time off the end
Logger.log('todaysDate: ' + todaysDate);
PropertiesService.getDocumentProperties().setProperty(todaysDate, theColumn);
var theColumnForTheDate = PropertiesService.getDocumentProperties()
.getProperty(todaysDate);
Logger.log('theColumnForTheDate: ' + theColumnForTheDate);