Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar: repeating event rrule & exdate fixes #1701

Merged
merged 4 commits into from
Jun 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Added UK Met Office Datapoint feed as a provider in the default weather module.
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
- Handle SIGTERM messages
- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays
- Fixed issues with calendar not displaying one-time changes to repeating events
>>>>>>> upstream/develop

## [2.7.1] - 2019-04-02
Expand Down
1 change: 1 addition & 0 deletions modules/default/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Module.register("calendar", {
}
} else if (notification === "FETCH_ERROR") {
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
this.loaded = true
} else if (notification === "INCORRECT_URL") {
Log.error("Calendar Error. Incorrect url: " + payload.url);
} else {
Expand Down
108 changes: 97 additions & 11 deletions modules/default/calendar/calendarfetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
// console.log(data);
newEvents = [];

var limitFunction = function(date, i) {return i < maximumEntries;};
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
var limitFunction = function(date, i) {return true;};

var eventDate = function(event, time) {
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
Expand Down Expand Up @@ -115,12 +116,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
startDate = startDate.startOf("day");
}

var title = "Event";
if (event.summary) {
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary;
} else if(event.description) {
title = event.description;
}
var title = getTitleFromEvent(event);

var excluded = false,
dateFilter = null;
Expand Down Expand Up @@ -176,8 +172,10 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
var geo = event.geo || false;
var description = event.description || false;

// Begin recurring event parsing
if (typeof event.rrule != "undefined" && event.rrule != null && !isFacebookBirthday) {
var rule = event.rrule;
var addedEvents = 0;

// can cause problems with e.g. birthdays before 1900
if(rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900 ||
Expand All @@ -186,19 +184,88 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
rule.options.dtstart.setYear(1900);
}

// For recurring events, get the set of start dates that fall within the range
// of dates we"re looking for.
var dates = rule.between(past, future, true, limitFunction);

// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it"s possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
// we"ll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don"t actually belong within
// our display range.
// Would be great if there was a better way to handle this.
if (event.recurrences != undefined)
{
var pastMoment = moment(past);
var futureMoment = moment(future);

for (var r in event.recurrences)
{
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) != true)
{
dates.push(new Date(r));
}
}
}


// Loop through the set of date entries to see which recurrences should be added to our event list.
for (var d in dates) {
startDate = moment(new Date(dates[d]));
var date = dates[d];
// ical.js started returning recurrences and exdates as ISOStrings without time information.
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
// (see https://github.com/peterbraden/ical.js/pull/84 )
var dateKey = date.toISOString().substring(0,10);
var curEvent = event;
var showRecurrence = true;

// Stop parsing this event's recurrences if we've already found maximumEntries worth of recurrences.
// (The logic below would still filter the extras, but the check is simple since we're already tracking the count)
if (addedEvents >= maximumEntries) {
break;
}

startDate = moment(date);

// For each date that we"re checking, it"s possible that there is a recurrence override for that one day.
if ((curEvent.recurrences != undefined) && (curEvent.recurrences[dateKey] != undefined))
{
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there"s no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateKey] != undefined))
{
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}

endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") == endDate.format("x")) {
endDate = endDate.endOf("day")
}

var recurrenceTitle = getTitleFromEvent(curEvent);

// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}

if (timeFilterApplies(now, endDate, dateFilter)) {
continue;
showRecurrence = false;
}

if (includePastEvents || endDate.format("x") > now) {
if ((showRecurrence === true) && (addedEvents < maximumEntries)) {
addedEvents++;
newEvents.push({
title: title,
title: recurrenceTitle,
startDate: startDate.format("x"),
endDate: endDate.format("x"),
fullDayEvent: isFullDayEvent(event),
Expand All @@ -210,6 +277,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
});
}
}
// end recurring event parsing
} else {
// console.log("Single event ...");
// Single event.
Expand Down Expand Up @@ -327,6 +395,24 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
return false;
};

/* getTitleFromEvent(event)
* Gets the title from the event.
*
* argument event object - The event object to check.
*
* return string - The title of the event, or "Event" if no title is found.
*/
var getTitleFromEvent = function (event) {
var title = "Event";
if (event.summary) {
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}

return title;
};

var testTitleByFilter = function (title, filter, useRegex, regexFlags) {
if (useRegex) {
// Assume if leading slash, there is also trailing slash
Expand Down
1 change: 1 addition & 0 deletions modules/default/calendar/node_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ module.exports = NodeHelper.create({
});

fetcher.onError(function(fetcher, error) {
console.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error)
self.sendSocketNotification("FETCH_ERROR", {
url: fetcher.url(),
error: error
Expand Down
4 changes: 1 addition & 3 deletions modules/default/calendar/vendor/ical.js/.travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
language: node_js
node_js:
- "0.10"
- "0.12"
- "4.2"
- "8.9"
install: npm install
23 changes: 13 additions & 10 deletions modules/default/calendar/vendor/ical.js/example.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
var ical = require('ical')
, months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
'use strict';

const ical = require('ical');
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function(err, data){
for (var k in data){
if (data.hasOwnProperty(k)){
var ev = data[k]
console.log("Conference", ev.summary, 'is in', ev.location, 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()] );
}
}
})
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
for (let k in data) {
if (data.hasOwnProperty(k)) {
var ev = data[k];
if (data[k].type == 'VEVENT') {
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);

}
}
}
});
118 changes: 118 additions & 0 deletions modules/default/calendar/vendor/ical.js/example_rrule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
var ical = require('./node-ical')
var moment = require('moment')

var data = ical.parseFile('./examples/example_rrule.ics');

// Complicated example demonstrating how to handle recurrence rules and exceptions.

for (var k in data) {

// When dealing with calendar recurrences, you need a range of dates to query against,
// because otherwise you can get an infinite number of calendar events.
var rangeStart = moment("2017-01-01");
var rangeEnd = moment("2017-12-31");


var event = data[k]
if (event.type === 'VEVENT') {

var title = event.summary;
var startDate = moment(event.start);
var endDate = moment(event.end);

// Calculate the duration of the event for use with recurring events.
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));

// Simple case - no recurrences, just print out the calendar event.
if (typeof event.rrule === 'undefined')
{
console.log('title:' + title);
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('duration:' + moment.duration(duration).humanize());
console.log();
}

// Complicated case - if an RRULE exists, handle multiple recurrences of the event.
else if (typeof event.rrule !== 'undefined')
{
// For recurring events, get the set of event start dates that fall within the range
// of dates we're looking for.
var dates = event.rrule.between(
rangeStart.toDate(),
rangeEnd.toDate(),
true,
function(date, i) {return true;}
)

// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. One way to handle this is
// to add *all* recurrence override entries into the set of dates that we check, and then later
// filter out any recurrences that don't actually belong within our range.
if (event.recurrences != undefined)
{
for (var r in event.recurrences)
{
// Only add dates that weren't already in the range we added from the rrule so that
// we don't double-add those events.
if (moment(new Date(r)).isBetween(rangeStart, rangeEnd) != true)
{
dates.push(new Date(r));
}
}
}

// Loop through the set of date entries to see which recurrences should be printed.
for(var i in dates) {

var date = dates[i];
var curEvent = event;
var showRecurrence = true;
var curDuration = duration;

startDate = moment(date);

// Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information)
var dateLookupKey = date.toISOString().substring(0, 10);

// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if ((curEvent.recurrences != undefined) && (curEvent.recurrences[dateLookupKey] != undefined))
{
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateLookupKey];
startDate = moment(curEvent.start);
curDuration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateLookupKey] != undefined))
{
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}

// Set the the title and the end date from either the regular event or the recurrence override.
var recurrenceTitle = curEvent.summary;
endDate = moment(parseInt(startDate.format("x")) + curDuration, 'x');

// If this recurrence ends before the start of the date range, or starts after the end of the date range,
// don't process it.
if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) {
showRecurrence = false;
}

if (showRecurrence === true) {

console.log('title:' + recurrenceTitle);
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
console.log('duration:' + moment.duration(curDuration).humanize());
console.log();
}

}
}
}
}


40 changes: 40 additions & 0 deletions modules/default/calendar/vendor/ical.js/examples/example_rrule.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:ical
X-WR-TIMEZONE:US/Central
X-WR-CALDESC:
BEGIN:VEVENT
UID:98765432-ABCD-DCBB-999A-987765432123
DTSTART;TZID=US/Central:20170601T090000
DTEND;TZID=US/Central:20170601T170000
DTSTAMP:20170727T044436Z
EXDATE;TZID=US/Central:20170706T090000,20170713T090000,20170720T090000,20
170803T090000
LAST-MODIFIED:20170727T044435Z
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;BYDAY=TH
SEQUENCE:0
SUMMARY:Recurring weekly meeting from June 1 - Aug 14 (except July 6, July 13, July 20, Aug 3)
END:VEVENT
BEGIN:VEVENT
UID:98765432-ABCD-DCBB-999A-987765432123
RECURRENCE-ID;TZID=US/Central:20170629T090000
DTSTART;TZID=US/Central:20170703T090000
DTEND;TZID=US/Central:20170703T120000
DTSTAMP:20170727T044436Z
LAST-MODIFIED:20170216T143445Z
SEQUENCE:0
SUMMARY:Last meeting in June moved to Monday July 3 and shortened to half day
END:VEVENT
BEGIN:VEVENT
UID:12354454-ABCD-DCBB-999A-2349872354897
DTSTART;TZID=US/Central:20171201T130000
DTEND;TZID=US/Central:20171201T150000
DTSTAMP:20170727T044436Z
LAST-MODIFIED:20170727T044435Z
SEQUENCE:0
SUMMARY:Single event on Dec 1
END:VEVENT
END:VCALENDAR
Loading