From 9d9cd44a2c0b74fc02793c8927e11927db0ebc22 Mon Sep 17 00:00:00 2001 From: plebcity Date: Tue, 3 Jun 2025 22:26:35 +0200 Subject: [PATCH 01/13] Refactored calendarfetcherutils to remove as many of the date conversions as possible and use moment tz when calculating recurring events, this will make debugging a lot easier and fixes problems from the past with offsets and DST not being handled properly. Also added some tests to test the behavior of the refactored methodes to make sure the correct event dates are returned --- CHANGELOG.md | 4 + .../default/calendar/calendarfetcherutils.js | 577 +++++------------- .../calendar/calendar_fetcher_utils_spec.js | 63 ++ 3 files changed, 222 insertions(+), 422 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a44c8b2e80..e70b05bbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ planned for 2025-07-01 - [refactor] Replace `ansis` with built-in function `util.styleText` (#3793) - [core] Integrate stuff from `vendor` and `fonts` folders into main `package.json`, simplifies install and maintaining dependencies (#3795) - [l10n] Complete translations (with the help of translation tools) (#3794) +- [refactor] Refactored `calendarfetcherutils` in Calendar module to handle timezones better + - Removed as many of the date conversions as possible + - Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly + - Added some tests to test the behavior of the refactored methodes to make sure the correct event dates are returned ### Fixed diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 3b037f13c0..c64818d1eb 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -2,7 +2,7 @@ * @external Moment */ const path = require("node:path"); -const moment = require("moment"); +const moment = require("moment-timezone"); const zoneTable = require(path.join(__dirname, "windowsZones.json")); const Log = require("../../../js/logger"); @@ -10,105 +10,114 @@ const Log = require("../../../js/logger"); const CalendarFetcherUtils = { /** - * Calculate the time correction, either dst/std or full day in cases where - * utc time is day before plus offset - * @param {object} event the event which needs adjustment - * @param {Date} date the date on which this event happens - * @returns {number} the necessary adjustment in hours + * Determine based on the title of an event if it should be excluded from the list of events + * TODO This seems like an overly complicated way to exclude events based on the title. + * @param {object} config the global config + * @param {string} title the title of the event + * @returns {object} excluded: true if the event should be excluded, false otherwise + * until: the date until the event should be excluded. */ - calculateTimezoneAdjustment (event, date) { - let adjustHours = 0; - // if a timezone was specified - if (!event.start.tz) { - Log.debug(" if no tz, guess based on now"); - event.start.tz = moment.tz.guess(); - } - Log.debug(`initial tz=${event.start.tz}`); - - // if there is a start date specified - if (event.start.tz) { - // if this is a windows timezone - if (event.start.tz.includes(" ")) { - // use the lookup table to get theIANA name as moment and date don't know MS timezones - let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz); - Log.debug(`corrected TZ=${tz}`); - // watch out for unregistered windows timezone names - // if we had a successful lookup - if (tz) { - // change the timezone to the IANA name - event.start.tz = tz; - // Log.debug("corrected timezone="+event.start.tz) - } - } - Log.debug(`corrected tz=${event.start.tz}`); - let current_offset = 0; // offset from TZ string or calculated - let mm = 0; // date with tz or offset - let start_offset = 0; // utc offset of created with tz - // if there is still an offset, lookup failed, use it - if (event.start.tz.startsWith("(")) { - const regex = /[+|-]\d*:\d*/; - const start_offsetString = event.start.tz.match(regex).toString().split(":"); - let start_offset = parseInt(start_offsetString[0]); - start_offset *= event.start.tz[1] === "-" ? -1 : 1; - adjustHours = start_offset; - Log.debug(`defined offset=${start_offset} hours`); - current_offset = start_offset; - event.start.tz = ""; - Log.debug(`ical offset=${current_offset} date=${date}`); - mm = moment(date); - let x = moment(new Date()).utcOffset(); - Log.debug(`net mins=${current_offset * 60 - x}`); - - mm = mm.add(x - current_offset * 60, "minutes"); - adjustHours = (current_offset * 60 - x) / 60; - event.start = mm.toDate(); - Log.debug(`adjusted date=${event.start}`); - } else { - // get the start time in that timezone - let es = moment(event.start); - // check for start date prior to start of daylight changing date - if (es.format("YYYY") < 2007) { - es.set("year", 2013); // if so, use a closer date + shouldEventBeExcluded (config, title) { + let filter = { + excluded: false, + until: null + }; + for (let f in config.excludedEvents) { + let filter = config.excludedEvents[f], + testTitle = title.toLowerCase(), + until = null, + useRegex = false, + regexFlags = "g"; + + if (filter instanceof Object) { + if (typeof filter.until !== "undefined") { + until = filter.until; } - Log.debug(`start date/time=${es.toDate()}`); - start_offset = moment.tz(es, event.start.tz).utcOffset(); - Log.debug(`start offset=${start_offset}`); - Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`); + if (typeof filter.regex !== "undefined") { + useRegex = filter.regex; + } - // get the specified date in that timezone - mm = moment.tz(moment(date), event.start.tz); - Log.debug(`event date=${mm.toDate()}`); - current_offset = mm.utcOffset(); - } - Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`); - - // if the offset is greater than 0, east of london - if (current_offset !== start_offset) { - // big offset - Log.debug("offset"); - let h = parseInt(mm.format("H")); - // check if the event time is less than the offset - if (h > 0 && h < Math.abs(current_offset) / 60) { - // if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time) - // we need to fix that - //adjustHours = 24; - // Log.debug("adjusting date") + // If additional advanced filtering is added in, this section + // must remain last as we overwrite the filter object with the + // filterBy string + if (filter.caseSensitive) { + filter = filter.filterBy; + testTitle = title; + } else if (useRegex) { + filter = filter.filterBy; + testTitle = title; + regexFlags += "i"; + } else { + filter = filter.filterBy.toLowerCase(); } - //-300 > -240 - //if (Math.abs(current_offset) > Math.abs(start_offset)){ - if (current_offset > start_offset) { - adjustHours -= 1; - Log.debug("adjust down 1 hour dst change"); - //} else if (Math.abs(current_offset) < Math.abs(start_offset)) { - } else if (current_offset < start_offset) { - adjustHours += 1; - Log.debug("adjust up 1 hour dst change"); + } else { + filter = filter.toLowerCase(); + } + + if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) { + if (until) { + filter.until = until; + } else { + filter.excluded = true; } + break; } } - Log.debug(`adjustHours=${adjustHours}`); - return adjustHours; + return filter; + }, + + /** + * This function returns a list of moments for a recurring event. + * @param {object} event the current event which is a recurring event + * @param {moment.Moment} pastLocalMoment The past date to search for recurring events + * @param {moment.Moment} futureLocalMoment The future date to search for recurring events + * @returns {moment.Moment[]} All moments for the recurring event + */ + getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment) { + const rule = event.rrule; + + // can cause problems with e.g. birthdays before 1900 + if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) { + rule.origOptions.dtstart.setYear(1900); + rule.options.dtstart.setYear(1900); + } + + let searchFromDate = pastLocalMoment.clone().subtract(1, "days").toDate(); + let searchToDate = futureLocalMoment.clone().add(1, "days").toDate(); + Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`); + + // if until is set, and its a full day event, force the time to midnight. rrule gets confused with non-00 offset + // looks like MS Outlook sets the until time incorrectly for fullday events + if ((rule.options.until !== undefined) && CalendarFetcherUtils.isFullDayEvent(event)) { + Log.debug("fixup rrule until"); + rule.options.until = moment(rule.options.until).clone().startOf("day").add(1, "day") + .toDate(); + } + + Log.debug("fix rrule start=", rule.options.dtstart); + Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate); + // fixup the exdate and recurrence date to local time too for post between() handling + // TODO figure out what this does + // CalendarFetcherUtils.fixEventtoLocal(event); + + Log.debug(`RRule: ${rule.toString()}`); + rule.options.tzid = null; // RRule gets *very* confused with timezones + + let dates = rule.between(searchFromDate, searchToDate, true, () => { + return true; + }); + + Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`); + + // shouldn't need this anymore, as RRULE not passed junk + dates = dates.filter((d) => { + return JSON.stringify(d) !== "null"; + }); + + // Dates are returned in UTC timezone but with localdatetime because tzid is null. + // So we map the date to a moment using the original timezone of the event. + return dates.map((d) => moment(d).tz(event.start.tz, true)); }, /** @@ -120,34 +129,32 @@ const CalendarFetcherUtils = { filterEvents (data, config) { const newEvents = []; - // 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 - const limitFunction = function (date, i) { - return true; - }; - const eventDate = function (event, time) { - return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time]).startOf("day") : moment(event[time]); + return CalendarFetcherUtils.isFullDayEvent(event) ? moment.tz(event[time], event[time].tz).startOf("day") : moment.tz(event[time], event[time].tz); }; Log.debug(`There are ${Object.entries(data).length} calendar entries.`); - const now = new Date(Date.now()); - const todayLocal = moment(now).startOf("day").toDate(); - const futureLocalDate - = moment(now) + const now = moment(); + const pastLocalMoment = config.includePastEvents ? now.clone().startOf("day").subtract(config.maximumNumberOfDays, "days") : now; + const futureLocalMoment + = now + .clone() .startOf("day") .add(config.maximumNumberOfDays, "days") - .subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat. - .toDate(); + // Subtract 1 second so that events that start on the middle of the night will not repeat. + .subtract(1, "seconds"); Object.entries(data).forEach(([key, event]) => { Log.debug("Processing entry..."); - let pastLocalDate = todayLocal; - if (config.includePastEvents) { - pastLocalDate = moment(now).startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); + const title = CalendarFetcherUtils.getTitleFromEvent(event); + Log.debug(`title: ${title}`); + + // Return quickly if event should be excluded. + let { excluded, eventFilterUntil } = this.shouldEventBeExcluded(config, title); + if (excluded) { + return; } // FIXME: Ugly fix to solve the facebook birthday issue. @@ -161,218 +168,47 @@ const CalendarFetcherUtils = { if (event.type === "VEVENT") { Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`); - let startMoment = eventDate(event, "start"); - let endMoment; + let eventStartMoment = eventDate(event, "start"); + let eventEndMoment; if (typeof event.end !== "undefined") { - endMoment = eventDate(event, "end"); + eventEndMoment = eventDate(event, "end"); } else if (typeof event.duration !== "undefined") { - endMoment = startMoment.clone().add(moment.duration(event.duration)); + eventEndMoment = eventStartMoment.clone().add(moment.duration(event.duration)); } else { if (!isFacebookBirthday) { // make copy of start date, separate storage area - endMoment = moment(startMoment.valueOf()); + eventEndMoment = eventStartMoment.clone(); } else { - endMoment = moment(startMoment).add(1, "days"); + eventEndMoment = eventStartMoment.clone().add(1, "days"); } } - Log.debug(`start: ${startMoment.toDate()}`); - Log.debug(`end:: ${endMoment.toDate()}`); + Log.debug(`start: ${eventStartMoment.toDate()}`); + Log.debug(`end:: ${eventEndMoment.toDate()}`); // Calculate the duration of the event for use with recurring events. - const durationMs = endMoment.valueOf() - startMoment.valueOf(); + const durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf(); Log.debug(`duration: ${durationMs}`); - // FIXME: Since the parsed json object from node-ical comes with time information - // this check could be removed (?) - if (event.start.length === 8) { - startMoment = startMoment.startOf("day"); - } - - const title = CalendarFetcherUtils.getTitleFromEvent(event); - Log.debug(`title: ${title}`); - - let excluded = false, - dateFilter = null; - - for (let f in config.excludedEvents) { - let filter = config.excludedEvents[f], - testTitle = title.toLowerCase(), - until = null, - useRegex = false, - regexFlags = "g"; - - if (filter instanceof Object) { - if (typeof filter.until !== "undefined") { - until = filter.until; - } - - if (typeof filter.regex !== "undefined") { - useRegex = filter.regex; - } - - // If additional advanced filtering is added in, this section - // must remain last as we overwrite the filter object with the - // filterBy string - if (filter.caseSensitive) { - filter = filter.filterBy; - testTitle = title; - } else if (useRegex) { - filter = filter.filterBy; - testTitle = title; - regexFlags += "i"; - } else { - filter = filter.filterBy.toLowerCase(); - } - } else { - filter = filter.toLowerCase(); - } - - if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) { - if (until) { - dateFilter = until; - } else { - excluded = true; - } - break; - } - } - - if (excluded) { - return; - } - const location = event.location || false; const geo = event.geo || false; const description = event.description || false; - let d1; - let d2; + // TODO This should be a seperate function. if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) { - const rule = event.rrule; - - const pastMoment = moment(pastLocalDate); - const futureMoment = moment(futureLocalDate); - - // can cause problems with e.g. birthdays before 1900 - if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) { - rule.origOptions.dtstart.setYear(1900); - 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. - - let pastLocal; - let futureLocal; - - if (CalendarFetcherUtils.isFullDayEvent(event)) { - Log.debug("fullday"); - // if full day event, only use the date part of the ranges - pastLocal = pastMoment.toDate(); - futureLocal = futureMoment.toDate(); - - Log.debug(`pastLocal: ${pastLocal}`); - Log.debug(`futureLocal: ${futureLocal}`); - } else { - // if we want past events - if (config.includePastEvents) { - // use the calculated past time for the between from - pastLocal = pastMoment.toDate(); - } else { - // otherwise use NOW.. cause we shouldn't use any before now - pastLocal = moment(now).toDate(); //now - } - futureLocal = futureMoment.toDate(); // future - } - const oneDayInMs = 24 * 60 * 60 * 1000; - d1 = new Date(new Date(pastLocal.valueOf() - oneDayInMs).getTime()); - d2 = new Date(new Date(futureLocal.valueOf() + oneDayInMs).getTime()); - Log.debug(`Search for recurring events between: ${d1} and ${d2}`); - - event.start = rule.options.dtstart; - - // if until is set, and its a full day event, force the time to midnight. rrule gets confused with non-00 offset - // looks like MS Outlook sets the until time incorrectly for fullday events - if ((rule.options.until !== undefined) && CalendarFetcherUtils.isFullDayEvent(event)) { - Log.debug("fixup rrule until"); - rule.options.until = new Date(new Date(moment(rule.options.until).startOf("day").add(1, "day")).getTime()); - } - - Log.debug("fix rrule start=", rule.options.dtstart); - Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate); - // fixup the exdate and recurrence date to local time too for post between() handling - CalendarFetcherUtils.fixEventtoLocal(event); - - Log.debug(`RRule: ${rule.toString()}`); - rule.options.tzid = null; // RRule gets *very* confused with timezones - - let dates = rule.between(d1, d2, true, () => { return true; }); - - Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`); - - // shouldn't need this anymore, as RRULE not passed junk - dates = dates.filter((d) => { - if (JSON.stringify(d) === "null") return false; - else return true; - }); - - // go thru all the rrule.between() dates and put back the tz offset removed so rrule.between would work - let datesLocal = []; - let offset = d1.getTimezoneOffset(); - Log.debug("offset =", offset); - dates.forEach((d) => { - let dtext = d.toISOString().slice(0, -5); - Log.debug(" date text form without tz=", dtext); - let dLocal = new Date(d.valueOf() + (offset * 60000)); - let offset2 = dLocal.getTimezoneOffset(); - Log.debug("date after offset applied=", dLocal); - if (offset !== offset2) { - // woops, dst/std switch - let delta = offset - offset2; - Log.debug("offset delta=", delta); - dLocal = new Date(d.valueOf() + ((offset - delta) * 60000)); - Log.debug("corrected normalized date=", dLocal); - } else Log.debug(" neutralized date=", dLocal); - datesLocal.push(dLocal); - }); - dates = datesLocal; - - - // 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. - // - // i don't think we will ever see this anymore (oct 2024) due to code fixes for rrule.between() - // - Log.debug("event.recurrences:", event.recurrences); - if (event.recurrences !== undefined) { - for (let dateKey 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. - let d = new Date(dateKey); - if (!moment(d).isBetween(d1, d2)) { - Log.debug("adding recurring event not found in between list =", d, " should not happen now using local dates oct 17,24"); - dates.push(d); - } - } - } + // Recurring event. + let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment); - // Loop through the set of date entries to see which recurrences should be added to our event list. - for (let d in dates) { - let date = dates[d]; + // Loop through the set of moment entries to see which recurrences should be added to our event list. + // TODO This should create an event per moment so we can change anything we want. + for (let m in moments) { let curEvent = event; - let curDurationMs = durationMs; let showRecurrence = true; + let recurringEventStartMoment = moments[m].tz(moment.tz.guess()).clone(); + let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms"); - let startMoment = moment(date); - - let dateKey = CalendarFetcherUtils.getDateKeyFromDate(date); + let dateKey = CalendarFetcherUtils.getDateKeyFromDate(recurringEventStartMoment.toDate()); Log.debug("event date dateKey=", dateKey); // For each date that we're checking, it's possible that there is a recurrence override for that one day. @@ -382,12 +218,8 @@ const CalendarFetcherUtils = { Log.debug("have a recurrence match for dateKey=", dateKey); // We found an override, so for this recurrence, use a potentially different title, start date, and duration. curEvent = curEvent.recurrences[dateKey]; - curEvent.start = new Date(new Date(curEvent.start.valueOf()).getTime()); - curEvent.end = new Date(new Date(curEvent.end.valueOf()).getTime()); - startMoment = CalendarFetcherUtils.getAdjustedStartMoment(curEvent.start, event); - endMoment = CalendarFetcherUtils.getAdjustedStartMoment(curEvent.end, event); - date = curEvent.start; - curDurationMs = new Date(endMoment).valueOf() - startMoment.valueOf(); + recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz, true).tz(moment.tz.guess()); + recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz, true).tz(moment.tz.guess()); } else { Log.debug("recurrence key ", dateKey, " doesn't match"); } @@ -400,25 +232,20 @@ const CalendarFetcherUtils = { showRecurrence = false; } } - Log.debug(`duration: ${curDurationMs}`); - - startMoment = CalendarFetcherUtils.getAdjustedStartMoment(date, event); - endMoment = moment(startMoment.valueOf() + curDurationMs); - - if (startMoment.valueOf() === endMoment.valueOf()) { - endMoment = endMoment.endOf("day"); + if (recurringEventStartMoment.valueOf() === recurringEventEndMoment.valueOf()) { + recurringEventEndMoment = recurringEventEndMoment.endOf("day"); } const recurrenceTitle = CalendarFetcherUtils.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 (endMoment.isBefore(pastLocal) || startMoment.isAfter(futureLocal)) { + if (recurringEventEndMoment.isBefore(pastLocalMoment) || recurringEventStartMoment.isAfter(futureLocalMoment)) { showRecurrence = false; } - if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) { + if (CalendarFetcherUtils.timeFilterApplies(now, recurringEventEndMoment, eventFilterUntil)) { showRecurrence = false; } @@ -426,8 +253,8 @@ const CalendarFetcherUtils = { Log.debug(`saving event: ${recurrenceTitle}`); newEvents.push({ title: recurrenceTitle, - startDate: startMoment.format("x"), - endDate: endMoment.format("x"), + startDate: recurringEventStartMoment.format("x"), + endDate: recurringEventEndMoment.format("x"), fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), recurringEvent: true, class: event.class, @@ -437,7 +264,7 @@ const CalendarFetcherUtils = { description: description }); } else { - Log.debug("not saving event ", recurrenceTitle, new Date(startMoment)); + Log.debug("not saving event ", recurrenceTitle, eventStartMoment); } Log.debug(" "); } @@ -448,47 +275,41 @@ const CalendarFetcherUtils = { // Log.debug("full day event") // if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00) - if (fullDayEvent && startMoment.valueOf() === endMoment.valueOf()) { - endMoment = endMoment.endOf("day"); + if (fullDayEvent && eventStartMoment.valueOf() === eventEndMoment.valueOf()) { + eventEndMoment = eventEndMoment.endOf("day"); } if (config.includePastEvents) { // Past event is too far in the past, so skip. - if (endMoment < pastLocalDate) { + if (eventEndMoment < pastLocalMoment) { return; } } else { // It's not a fullday event, and it is in the past, so skip. - if (!fullDayEvent && endMoment < now) { + if (!fullDayEvent && eventEndMoment < now) { return; } // It's a fullday event, and it is before today, So skip. - if (fullDayEvent && endMoment <= todayLocal) { + if (fullDayEvent && eventEndMoment <= now.startOf("day")) { return; } } // It exceeds the maximumNumberOfDays limit, so skip. - if (startMoment > futureLocalDate) { + if (eventStartMoment > futureLocalMoment) { return; } - if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) { + if (CalendarFetcherUtils.timeFilterApplies(now, eventEndMoment, eventFilterUntil)) { return; } - // get correction for date saving and dst change between now and then - let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startMoment.toDate()); - // This shouldn't happen - if (adjustHours) { - Log.warn(`Unexpected timezone adjustment of ${adjustHours} hours on non-recurring event`); - } // Every thing is good. Add it to the list. newEvents.push({ title: title, - startDate: startMoment.add(adjustHours, "hours").format("x"), - endDate: endMoment.add(adjustHours, "hours").format("x"), + startDate: eventStartMoment.format("x"), + endDate: eventEndMoment.format("x"), fullDayEvent: fullDayEvent, recurringEvent: false, class: event.class, @@ -599,7 +420,6 @@ const CalendarFetcherUtils = { // get our runtime timezone offset const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); let startday = date.getDate(); - let adjustment = 0; Log.debug(" day of month=", (`0${startday}`).slice(-2), " nowDiff=", nowDiff, ` start time=${date.toString().split(" ")[4].slice(0, 2)}`); Log.debug("date string= ", date.toString()); Log.debug("date iso string ", date.toISOString()); @@ -628,93 +448,6 @@ const CalendarFetcherUtils = { return h * 60 + (h > 0 ? +m : -m); }, - /** - * fixup the date start moment after rrule.between returns date array - * @param {Date} date object from rrule.between results - * the event object it came from - * @param {object} event - The event object it came from. - * @returns {Moment} moment object - */ - getAdjustedStartMoment (date, event) { - - let startMoment = moment(date); - - Log.debug("startMoment pre=", startMoment); - // get our runtime timezone offset - const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); // 10/18 16:49, 300 - let eventDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(event.end.tz); // watch out, start tz is cleared to handle rrule 120 23:49 - - Log.debug("tz diff event=", eventDiff, " local=", nowDiff, " end event timezone=", event.end.tz); - - // if the diffs are different (not same tz for processing as event) - if (nowDiff !== eventDiff) { - // if signs are different - if (Math.sign(nowDiff) !== Math.sign(eventDiff)) { - // its the accumulated total - Log.debug("diff signs, accumulate"); - eventDiff = Math.abs(eventDiff) + Math.abs(nowDiff); - // sign of diff depends on where you are looking at which event. - // australia looking at US, add to get same time - Log.debug("new different event diff=", eventDiff); - if (Math.sign(nowDiff) === -1) { - eventDiff *= -1; - // US looking at australia event have to subtract - Log.debug("new diff, same sign, total event diff=", eventDiff); - } - } - else { - // signs are the same, all east of UTC or all west of UTC - // if the signs are negative (west of UTC) - Log.debug("signs are the same"); - if (Math.sign(eventDiff) === -1) { - //if west, looking at more west - // -350 <-300 - if (nowDiff < eventDiff) { - //-600 -420 - //300 -300 -360 +300 - eventDiff = nowDiff - eventDiff; //-180 - Log.debug("now looking back east delta diff=", eventDiff); - } - else { - Log.debug("now looking more west"); - eventDiff = Math.abs(eventDiff - nowDiff); - } - } else { - Log.debug("signs are both positive"); - // signs are positive (east of UTC) - // berlin < sydney - if (nowDiff < eventDiff) { - // germany vs australia - eventDiff = -(eventDiff - nowDiff); - } - else { - // australia vs germany - //eventDiff = eventDiff; //- nowDiff - } - } - } - startMoment = moment.tz(new Date(date.valueOf() + (eventDiff * (60 * 1000))), event.end.tz); - } else { - Log.debug("same tz event and display"); - eventDiff = 0; - startMoment = moment.tz(new Date(date.valueOf() - (eventDiff * (60 * 1000))), event.end.tz); - } - Log.debug("startMoment post=", startMoment); - return startMoment; - }, - - /** - * Lookup iana tz from windows - * @param {string} msTZName the timezone name to lookup - * @returns {string|null} the iana name or null of none is found - */ - getIanaTZFromMS (msTZName) { - // Get hash entry - const he = zoneTable[msTZName]; - // If found return iana name, else null - return he ? he.iana[0] : null; - }, - /** * Gets the title from the event. * @param {object} event The event object to check. @@ -754,8 +487,8 @@ const CalendarFetcherUtils = { /** * Determines if the user defined time filter should apply - * @param {Date} now Date object using previously created object for consistency - * @param {Moment} endDate Moment object representing the event end date + * @param {moment.Moment} now Date object using previously created object for consistency + * @param {moment.Moment} endDate Moment object representing the event end date * @param {string} filter The time to subtract from the end date to determine if an event should be shown * @returns {boolean} True if the event should be filtered out, false otherwise */ @@ -766,7 +499,7 @@ const CalendarFetcherUtils = { increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js filterUntil = moment(endDate.format()).subtract(value, increment); - return now < filterUntil.toDate(); + return now < filterUntil; } return false; diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index 344e11d576..691765b7e1 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -1,5 +1,8 @@ global.moment = require("moment-timezone"); +const ical = require("node-ical"); +const { expect } = require("playwright/test"); +const moment = require("moment-timezone"); const CalendarFetcherUtils = require("../../../../../modules/default/calendar/calendarfetcherutils"); describe("Calendar fetcher utils test", () => { @@ -49,5 +52,65 @@ describe("Calendar fetcher utils test", () => { expect(filteredEvents[0].title).toBe("ongoingEvent"); expect(filteredEvents[1].title).toBe("upcomingEvent"); }); + + it("should return the correct times when recurring events pass through daylight saving time", () => { + const data = ical.parseICS(`BEGIN:VEVENT +DTSTART;TZID=Europe/Amsterdam:20250311T090000 +DTEND;TZID=Europe/Amsterdam:20250311T091500 +RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE,SA,SU +DTSTAMP:20250531T091103Z +ORGANIZER;CN=test:mailto:test@test.com +UID:67e65a1d-b889-4451-8cab-5518cecb9c66 +CREATED:20230111T114612Z +DESCRIPTION:Test +LAST-MODIFIED:20250528T071312Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Test +TRANSP:OPAQUE +END:VEVENT`); + + const filteredEvents = CalendarFetcherUtils.filterEvents(data, defaultConfig); + + const januaryFirst = filteredEvents.filter((event) => moment.unix(event.startDate / 1000).format("MM-DD") === "01-01"); + const julyFirst = filteredEvents.filter((event) => moment.unix(event.startDate / 1000).format("MM-DD") === "07-01"); + + let januaryMoment = moment(`${moment.unix(januaryFirst[0].startDate / 1000).format("YYYY")}-01-01T09:00:00`) + .tz("Europe/Amsterdam", true) // Convert to Europe/Amsterdam timezone (see event ical) but keep 9 o'clock + .tz(moment.tz.guess()); // Convert to guessed timezone as that is used in the filterEvents + + let julyMoment = moment(`${moment.unix(julyFirst[0].startDate / 1000).format("YYYY")}-07-01T09:00:00`) + .tz("Europe/Amsterdam", true) // Convert to Europe/Amsterdam timezone (see event ical) but keep 9 o'clock + .tz(moment.tz.guess()); // Convert to guessed timezone as that is used in the filterEvents + + expect(januaryFirst[0].startDate).toEqual(januaryMoment.format("x")); + expect(julyFirst[0].startDate).toEqual(julyMoment.format("x")); + }); + + it("should return the correct moments based on the timezone given", () => { + const data = ical.parseICS(`BEGIN:VEVENT +DTSTART;TZID=Europe/Amsterdam:20250311T090000 +DTEND;TZID=Europe/Amsterdam:20250311T091500 +RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE,SA,SU +DTSTAMP:20250531T091103Z +ORGANIZER;CN=test:mailto:test@test.com +UID:67e65a1d-b889-4451-8cab-5518cecb9c66 +CREATED:20230111T114612Z +DESCRIPTION:Test +LAST-MODIFIED:20250528T071312Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Test +TRANSP:OPAQUE +END:VEVENT`); + + const moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(data["67e65a1d-b889-4451-8cab-5518cecb9c66"], moment(), moment().add(365, "days")); + + const januaryFirst = moments.filter((m) => m.format("MM-DD") === "01-01"); + const julyFirst = moments.filter((m) => m.format("MM-DD") === "07-01"); + + expect(januaryFirst[0].toISOString(true)).toContain("09:00:00.000+01:00"); + expect(julyFirst[0].toISOString(true)).toContain("09:00:00.000+02:00"); + }); }); }); From bb168eec46765986c777e78ecaae834fe987f2cf Mon Sep 17 00:00:00 2001 From: plebcity Date: Wed, 4 Jun 2025 20:58:10 +0200 Subject: [PATCH 02/13] Refactored calendar.js aswell to convert the unix UTC start and end date of an event to the proper timezone. This should now display the correct times in the DOM --- modules/default/calendar/calendar.js | 140 +++++++++--------- .../default/calendar/calendarfetcherutils.js | 8 +- .../calendar/calendar_fetcher_utils_spec.js | 12 +- 3 files changed, 79 insertions(+), 81 deletions(-) diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index e2f921db28..6504bfafe9 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -1,5 +1,7 @@ /* global CalendarUtils */ +const moment = require("moment-timezone"); + Module.register("calendar", { // Define module defaults defaults: { @@ -77,7 +79,7 @@ Module.register("calendar", { // Define required scripts. getScripts () { - return ["calendarutils.js", "moment.js"]; + return ["calendarutils.js", "moment-timezone.js"]; }, // Define required translations. @@ -215,11 +217,6 @@ Module.register("calendar", { this.updateDom(this.config.animationSpeed); }, - eventEndingWithinNextFullTimeUnit (event, ONE_DAY) { - const now = new Date(); - return event.endDate - now <= ONE_DAY; - }, - // Override dom generator. getDom () { const ONE_SECOND = 1000; // 1,000 milliseconds @@ -258,7 +255,9 @@ Module.register("calendar", { let lastSeenDate = ""; events.forEach((event, index) => { - const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat); + const eventStartDateMoment = this.timestampToMoment(event.startDate); + const eventEndDateMoment = this.timestampToMoment(event.endDate); + const dateAsString = eventStartDateMoment.format(this.config.dateFormat); if (this.config.timeFormat === "dateheaders") { if (lastSeenDate !== dateAsString) { const dateRow = document.createElement("tr"); @@ -340,7 +339,7 @@ Module.register("calendar", { repeatingCountTitle = this.countTitleForUrl(event.url); if (repeatingCountTitle !== "") { - const thisYear = new Date(parseInt(event.startDate)).getFullYear(), + const thisYear = eventStartDateMoment.year(), yearDiff = thisYear - event.firstYear; repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`; @@ -395,14 +394,14 @@ Module.register("calendar", { timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; timeWrapper.style.paddingLeft = "2px"; timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; - timeWrapper.innerHTML = moment(event.startDate, "x").format("LT"); + timeWrapper.innerHTML = eventStartDateMoment.format("LT"); // Add endDate to dataheaders if showEnd is enabled if (this.config.showEnd) { if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { // no duration here, don't display end } else { - timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`; + timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`; } } @@ -415,17 +414,17 @@ Module.register("calendar", { const timeWrapper = document.createElement("td"); eventWrapper.appendChild(titleWrapper); - const now = new Date(); + const now = moment(); if (this.config.timeFormat === "absolute") { // Use dateFormat - timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat)); + timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat)); // Add end time if showEnd if (this.config.showEnd) { // and has a duation if (event.startDate !== event.endDate) { timeWrapper.innerHTML += "-"; - timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat)); + timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat)); } } @@ -433,26 +432,26 @@ Module.register("calendar", { if (event.fullDayEvent) { //subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day event.endDate -= ONE_SECOND; - timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat)); + timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat)); // only show end if requested and allowed and the dates are different - if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && moment(event.startDate, "x").format("YYYYMMDD") !== moment(event.endDate, "x").format("YYYYMMDD")) { + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) { timeWrapper.innerHTML += "-"; - timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat)); + timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat)); } else - if ((moment(event.startDate, "x").format("YYYYMMDD") !== moment(event.endDate, "x").format("YYYYMMDD")) && (moment(event.startDate, "x") < moment(now, "x"))) { - timeWrapper.innerHTML = CalendarUtils.capFirst(moment(now, "x").format(this.config.fullDayEventDateFormat)); + if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) { + timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat)); } - } else if (this.config.getRelative > 0 && event.startDate < now) { + } else if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) { // Ongoing and getRelative is set timeWrapper.innerHTML = CalendarUtils.capFirst( this.translate("RUNNING", { fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, - timeUntilEnd: moment(event.endDate, "x").fromNow(true) + timeUntilEnd: eventEndDateMoment.fromNow(true) }) ); - } else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) { + } else if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) { // Within urgency days - timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow()); + timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.fromNow()); } if (event.fullDayEvent && this.config.nextDaysRelative) { // Full days events within the next two days @@ -460,9 +459,9 @@ Module.register("calendar", { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY")); } else if (event.yesterday) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY")); - } else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) { + } else if (event.tomorrow) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW")); - } else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) { + } else if (event.dayAfterTomorrow) { if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); } @@ -470,15 +469,15 @@ Module.register("calendar", { } } else { // Show relative times - if (event.startDate >= now || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) { + if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) { // Use relative time if (!this.config.hideTime && !event.fullDayEvent) { Log.debug("event not hidden and not fullday"); - timeWrapper.innerHTML = `${CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }))}`; + timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`; } else { Log.debug("event full day or hidden"); timeWrapper.innerHTML = `${CalendarUtils.capFirst( - moment(event.startDate, "x").calendar(null, { + eventStartDateMoment.calendar(null, { sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`, nextDay: `[${this.translate("TOMORROW")}]`, nextWeek: "dddd", @@ -488,7 +487,7 @@ Module.register("calendar", { } if (event.fullDayEvent) { // Full days events within the next two days - if (event.today || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) { + if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY")); } else if (event.dayBeforeYesterday) { if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") { @@ -496,25 +495,25 @@ Module.register("calendar", { } } else if (event.yesterday) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY")); - } else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) { + } else if (event.tomorrow) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW")); - } else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) { + } else if (event.dayAfterTomorrow) { if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); } } Log.info("event fullday"); - } else if (event.startDate - now < this.config.getRelative * ONE_HOUR) { + } else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) { Log.info("not full day but within getrelative size"); // If event is within getRelative hours, display 'in xxx' time format or moment.fromNow() - timeWrapper.innerHTML = `${CalendarUtils.capFirst(moment(event.startDate, "x").fromNow())}`; + timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`; } } else { // Ongoing event timeWrapper.innerHTML = CalendarUtils.capFirst( this.translate("RUNNING", { fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, - timeUntilEnd: moment(event.endDate, "x").fromNow(true) + timeUntilEnd: eventEndDateMoment.fromNow(true) }) ); } @@ -593,46 +592,46 @@ Module.register("calendar", { return false; }, + /** + * converts the given timestamp to a moment with a timezone + * @param timestamp timestamp from an event + * @returns {moment.Moment} moment with a timezone + */ + timestampToMoment (timestamp) { + return moment.unix(timestamp).tz(moment.tz.guess()); + }, + /** * Creates the sorted list of all events. * @param {boolean} limitNumberOfEntries Whether to filter returned events for display. * @returns {object[]} Array with events. */ createEventList (limitNumberOfEntries) { - const ONE_SECOND = 1000; // 1,000 milliseconds - const ONE_MINUTE = ONE_SECOND * 60; - const ONE_HOUR = ONE_MINUTE * 60; - const ONE_DAY = ONE_HOUR * 24; + let now = moment(); + let today = now.clone().startOf("day"); + let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days"); - let now, today, future; - if (this.config.forceUseCurrentTime || this.defaults.forceUseCurrentTime) { - now = new Date(); - today = moment().startOf("day"); - future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); - } else { - now = new Date(Date.now()); // Can use overridden time - today = moment(now).startOf("day"); - future = moment(now).startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); - } let events = []; for (const calendarUrl in this.calendarData) { const calendar = this.calendarData[calendarUrl]; let remainingEntries = this.maximumEntriesForUrl(calendarUrl); - let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY; + let maxPastDaysCompare = now.clone().subtract(this.maximumPastDaysForUrl(calendarUrl), "days"); let by_url_calevents = []; for (const e in calendar) { const event = JSON.parse(JSON.stringify(calendar[e])); // clone object + const eventStartDateMoment = this.timestampToMoment(event.startDate); + const eventEndDateMoment = this.timestampToMoment(event.endDate); if (this.config.hidePrivate && event.class === "PRIVATE") { // do not add the current event, skip it continue; } if (limitNumberOfEntries) { - if (event.endDate < maxPastDaysCompare) { + if (eventEndDateMoment.isBefore(maxPastDaysCompare)) { continue; } - if (this.config.hideOngoing && event.startDate < now) { + if (this.config.hideOngoing && eventStartDateMoment.isBefore(now)) { continue; } if (this.config.hideDuplicates && this.listContainsEvent(events, event)) { @@ -641,47 +640,46 @@ Module.register("calendar", { } event.url = calendarUrl; - event.today = event.startDate >= today && event.startDate < today + ONE_DAY; - event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY; - event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today; - event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY; - event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY; + event.today = eventStartDateMoment.isSame(now, "d"); + event.dayBeforeYesterday = eventStartDateMoment.isSame(now.clone().subtract(2, "days"), "d"); + event.yesterday = eventStartDateMoment.isSame(now.clone().subtract(1, "days"), "d"); + event.tomorrow = eventStartDateMoment.isSame(now.clone().add(1, "days"), "d"); + event.dayAfterTomorrow = eventStartDateMoment.isSame(now.clone().add(2, "days"), "d"); /* * if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days, * otherwise, esp. in dateheaders mode it is not clear how long these events are. */ - const maxCount = Math.round((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1; + const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days") + 1; if (this.config.sliceMultiDayEvents && maxCount > 1) { const splitEvents = []; let midnight - = moment(event.startDate, "x") + = eventStartDateMoment .clone() .startOf("day") .add(1, "day") - .endOf("day") - .format("x"); + .endOf("day"); let count = 1; - while (event.endDate > midnight) { + while (eventEndDateMoment.isAfter(midnight)) { const thisEvent = JSON.parse(JSON.stringify(event)); // clone object - thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY; - thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY; - thisEvent.endDate = moment(midnight, "x").clone().subtract(1, "day").format("x"); + thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d"); + thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d"); + thisEvent.endDate = midnight.clone().subtract(1, "day").unix(); thisEvent.title += ` (${count}/${maxCount})`; splitEvents.push(thisEvent); - event.startDate = midnight; + event.startDate = midnight.unix(); count += 1; - midnight = moment(midnight, "x").add(1, "day").endOf("day").format("x"); // next day + midnight = midnight.clone().add(1, "day").endOf("day").unix(); // next day } // Last day event.title += ` (${count}/${maxCount})`; - event.today += event.startDate >= today && event.startDate < today + ONE_DAY; - event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY; + event.today += this.timestampToMoment(event.startDate).isSame(now, "d"); + event.tomorrow = this.timestampToMoment(event.startDate).isSame(now.clone().add(1, "days"), "d"); splitEvents.push(event); for (let splitEvent of splitEvents) { - if (splitEvent.endDate > now && splitEvent.endDate <= future) { + if (this.timestampToMoment(splitEvent.endDate).isAfter(now) && this.timestampToMoment(splitEvent.endDate).isSameOrBefore(future)) { by_url_calevents.push(splitEvent); } } @@ -716,16 +714,16 @@ Module.register("calendar", { */ if (this.config.limitDays > 0) { let newEvents = []; - let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD"); + let lastDate = today.clone().subtract(1, "days"); let days = 0; for (const ev of events) { - let eventDate = moment(ev.startDate, "x").format("YYYYMMDD"); + let eventDate = this.timestampToMoment(ev.startDate); /* * if date of event is later than lastdate * check if we already are showing max unique days */ - if (eventDate > lastDate) { + if (eventDate.isAfter(lastDate)) { // if the only entry in the first day is a full day event that day is not counted as unique if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) { days--; diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index c64818d1eb..a825a0ba7c 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -253,8 +253,8 @@ const CalendarFetcherUtils = { Log.debug(`saving event: ${recurrenceTitle}`); newEvents.push({ title: recurrenceTitle, - startDate: recurringEventStartMoment.format("x"), - endDate: recurringEventEndMoment.format("x"), + startDate: recurringEventStartMoment.unix(), + endDate: recurringEventEndMoment.unix(), fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), recurringEvent: true, class: event.class, @@ -308,8 +308,8 @@ const CalendarFetcherUtils = { // Every thing is good. Add it to the list. newEvents.push({ title: title, - startDate: eventStartMoment.format("x"), - endDate: eventEndMoment.format("x"), + startDate: eventStartMoment.unix(), + endDate: eventEndMoment.unix(), fullDayEvent: fullDayEvent, recurringEvent: false, class: event.class, diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index 691765b7e1..14744d8c4b 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -72,19 +72,19 @@ END:VEVENT`); const filteredEvents = CalendarFetcherUtils.filterEvents(data, defaultConfig); - const januaryFirst = filteredEvents.filter((event) => moment.unix(event.startDate / 1000).format("MM-DD") === "01-01"); - const julyFirst = filteredEvents.filter((event) => moment.unix(event.startDate / 1000).format("MM-DD") === "07-01"); + const januaryFirst = filteredEvents.filter((event) => moment.unix(event.startDate).format("MM-DD") === "01-01"); + const julyFirst = filteredEvents.filter((event) => moment.unix(event.startDate).format("MM-DD") === "07-01"); - let januaryMoment = moment(`${moment.unix(januaryFirst[0].startDate / 1000).format("YYYY")}-01-01T09:00:00`) + let januaryMoment = moment(`${moment.unix(januaryFirst[0].startDate).format("YYYY")}-01-01T09:00:00`) .tz("Europe/Amsterdam", true) // Convert to Europe/Amsterdam timezone (see event ical) but keep 9 o'clock .tz(moment.tz.guess()); // Convert to guessed timezone as that is used in the filterEvents - let julyMoment = moment(`${moment.unix(julyFirst[0].startDate / 1000).format("YYYY")}-07-01T09:00:00`) + let julyMoment = moment(`${moment.unix(julyFirst[0].startDate).format("YYYY")}-07-01T09:00:00`) .tz("Europe/Amsterdam", true) // Convert to Europe/Amsterdam timezone (see event ical) but keep 9 o'clock .tz(moment.tz.guess()); // Convert to guessed timezone as that is used in the filterEvents - expect(januaryFirst[0].startDate).toEqual(januaryMoment.format("x")); - expect(julyFirst[0].startDate).toEqual(julyMoment.format("x")); + expect(januaryFirst[0].startDate).toEqual(januaryMoment.unix()); + expect(julyFirst[0].startDate).toEqual(julyMoment.unix()); }); it("should return the correct moments based on the timezone given", () => { From 93c833bdc0dab6a55de3eae1f45da27881236bce Mon Sep 17 00:00:00 2001 From: plebcity Date: Wed, 4 Jun 2025 21:34:01 +0200 Subject: [PATCH 03/13] Removed unused functions and fixed a small issue with recurrence override handling --- modules/default/calendar/calendar.js | 2 - .../default/calendar/calendarfetcherutils.js | 121 +++--------------- 2 files changed, 20 insertions(+), 103 deletions(-) diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 6504bfafe9..9ac8670148 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -1,7 +1,5 @@ /* global CalendarUtils */ -const moment = require("moment-timezone"); - Module.register("calendar", { // Define module defaults defaults: { diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index a825a0ba7c..c68648c076 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -97,9 +97,6 @@ const CalendarFetcherUtils = { Log.debug("fix rrule start=", rule.options.dtstart); Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate); - // fixup the exdate and recurrence date to local time too for post between() handling - // TODO figure out what this does - // CalendarFetcherUtils.fixEventtoLocal(event); Log.debug(`RRule: ${rule.toString()}`); rule.options.tzid = null; // RRule gets *very* confused with timezones @@ -115,9 +112,12 @@ const CalendarFetcherUtils = { return JSON.stringify(d) !== "null"; }); + console.log(dates); + console.log(event); + // Dates are returned in UTC timezone but with localdatetime because tzid is null. // So we map the date to a moment using the original timezone of the event. - return dates.map((d) => moment(d).tz(event.start.tz, true)); + return dates.map((d) => (event.start.tz ? moment(d).tz(event.start.tz, true) : moment(d))); }, /** @@ -200,9 +200,13 @@ const CalendarFetcherUtils = { // Recurring event. let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment); + console.log(moments); + // Loop through the set of moment entries to see which recurrences should be added to our event list. // TODO This should create an event per moment so we can change anything we want. for (let m in moments) { + console.log(typeof moments[m]); + console.log(moments[m]); let curEvent = event; let showRecurrence = true; let recurringEventStartMoment = moments[m].tz(moment.tz.guess()).clone(); @@ -218,8 +222,17 @@ const CalendarFetcherUtils = { Log.debug("have a recurrence match for dateKey=", dateKey); // We found an override, so for this recurrence, use a potentially different title, start date, and duration. curEvent = curEvent.recurrences[dateKey]; - recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz, true).tz(moment.tz.guess()); - recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz, true).tz(moment.tz.guess()); + // Some event start/end dates don't have timezones + if (curEvent.start.tz) { + recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(moment.tz.guess()); + } else { + recurringEventStartMoment = moment(curEvent.start).tz(moment.tz.guess()); + } + if (curEvent.end.tz) { + recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(moment.tz.guess()); + } else { + recurringEventEndMoment = moment(curEvent.end).tz(moment.tz.guess()); + } } else { Log.debug("recurrence key ", dateKey, " doesn't match"); } @@ -329,87 +342,6 @@ const CalendarFetcherUtils = { return newEvents; }, - /** - * Fixes the event fields that have dates to use local time - * before calling rrule.between. - * @param {object} event - The event being processed. - * @returns {void} - */ - fixEventtoLocal (event) { - // if there are excluded dates, their date is incorrect and possibly key as well. - if (event.exdate !== undefined) { - Object.keys(event.exdate).forEach((dateKey) => { - // get the date - let exdate = event.exdate[dateKey]; - Log.debug("exdate w key=", exdate); - //exdate=CalendarFetcherUtils.convertDateToLocalTime(exdate, event.end.tz) - exdate = new Date(new Date(exdate.valueOf() - ((120 * 60 * 1000))).getTime()); - Log.debug("new exDate item=", exdate, " with old key=", dateKey); - let newkey = exdate.toISOString().slice(0, 10); - if (newkey !== dateKey) { - Log.debug("new exDate item=", exdate, ` key=${newkey}`); - event.exdate[newkey] = exdate; - //delete event.exdate[dateKey] - } - }); - Log.debug("updated exdate list=", event.exdate); - } - if (event.recurrences) { - Object.keys(event.recurrences).forEach((dateKey) => { - let exdate = event.recurrences[dateKey]; - //exdate=new Date(new Date(exdate.valueOf()-(60*60*1000)).getTime()) - Log.debug("new recurrence item=", exdate, " with old key=", dateKey); - exdate.start = CalendarFetcherUtils.convertDateToLocalTime(exdate.start, exdate.start.tz); - exdate.end = CalendarFetcherUtils.convertDateToLocalTime(exdate.end, exdate.end.tz); - Log.debug("adjusted recurringEvent start=", exdate.start, " end=", exdate.end); - }); - } - Log.debug("modified recurrences before rrule.between", event.recurrences); - }, - - /** - * convert a UTC date to local time - * BEFORE calling rrule.between - * @param {Date} date The date to convert - * @param {string} tz The timezone string to convert the date to. - * @returns {Date} updated date object - */ - convertDateToLocalTime (date, tz) { - let delta_tz_offset = 0; - let now_offset = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); - let event_offset = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(tz); - Log.debug("date to convert=", date); - if (Math.sign(now_offset) !== Math.sign(event_offset)) { - delta_tz_offset = Math.abs(now_offset) + Math.abs(event_offset); - } else { - // signs are the same - // if negative - if (Math.sign(now_offset) === -1) { - // la looking at chicago - if (now_offset < event_offset) { // 5 -7 - delta_tz_offset = now_offset - event_offset; - } - else { //7 -5 , chicago looking at LA - delta_tz_offset = event_offset - now_offset; - } - } - else { - // berlin looking at sydney - if (now_offset < event_offset) { // 5 -7 - delta_tz_offset = event_offset - now_offset; - Log.debug("less delta=", delta_tz_offset); - } - else { // 11 - 2, sydney looking at berlin - delta_tz_offset = -(now_offset - event_offset); - Log.debug("more delta=", delta_tz_offset); - } - } - } - const newdate = new Date(new Date(date.valueOf() + (delta_tz_offset * 60 * 1000)).getTime()); - Log.debug("modified date =", newdate); - return newdate; - }, - /** * get the exdate/recurrence hash key from the date object * BEFORE calling rrule.between @@ -418,9 +350,8 @@ const CalendarFetcherUtils = { */ getDateKeyFromDate (date) { // get our runtime timezone offset - const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); let startday = date.getDate(); - Log.debug(" day of month=", (`0${startday}`).slice(-2), " nowDiff=", nowDiff, ` start time=${date.toString().split(" ")[4].slice(0, 2)}`); + Log.debug(" day of month=", (`0${startday}`).slice(-2), ` start time=${date.toString().split(" ")[4].slice(0, 2)}`); Log.debug("date string= ", date.toString()); Log.debug("date iso string ", date.toISOString()); // if the dates are different @@ -436,18 +367,6 @@ const CalendarFetcherUtils = { return date.toISOString().substring(0, 8) + (`0${startday}`).slice(-2); }, - /** - * get the timezone offset from the timezone string - * @param {string} timeZone The timezone string - * @returns {number} The numerical offset in minutes from UTC. - */ - getTimezoneOffsetFromTimezone (timeZone) { - const str = new Date().toLocaleString("en", { timeZone, timeZoneName: "longOffset" }); - Log.debug("tz offset=", str); - const [_, h, m] = str.match(/([+-]\d+):(\d+)$/) || ["", "+00", "00"]; - return h * 60 + (h > 0 ? +m : -m); - }, - /** * Gets the title from the event. * @param {object} event The event object to check. From 7f22e9acfce1f7cd76e669129bb4c9a7685f652b Mon Sep 17 00:00:00 2001 From: Koen Konst Date: Thu, 5 Jun 2025 13:25:19 +0200 Subject: [PATCH 04/13] Fixed script issue in calendar.js, both moment and moment-timezone data should be loaded. Also fixed a bug when determining new recurring events with an original timezone --- modules/default/calendar/calendar.js | 2 +- modules/default/calendar/calendarfetcherutils.js | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 9ac8670148..46a4d689af 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -77,7 +77,7 @@ Module.register("calendar", { // Define required scripts. getScripts () { - return ["calendarutils.js", "moment-timezone.js"]; + return ["calendarutils.js", "moment-timezone.js", "moment.js"]; }, // Define required translations. diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index c68648c076..becf7cec80 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -112,12 +112,9 @@ const CalendarFetcherUtils = { return JSON.stringify(d) !== "null"; }); - console.log(dates); - console.log(event); - // Dates are returned in UTC timezone but with localdatetime because tzid is null. // So we map the date to a moment using the original timezone of the event. - return dates.map((d) => (event.start.tz ? moment(d).tz(event.start.tz, true) : moment(d))); + return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(moment.tz.guess(), true))); }, /** @@ -200,13 +197,9 @@ const CalendarFetcherUtils = { // Recurring event. let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment); - console.log(moments); - // Loop through the set of moment entries to see which recurrences should be added to our event list. // TODO This should create an event per moment so we can change anything we want. for (let m in moments) { - console.log(typeof moments[m]); - console.log(moments[m]); let curEvent = event; let showRecurrence = true; let recurringEventStartMoment = moments[m].tz(moment.tz.guess()).clone(); From 43572212d17832ec673f5ce16b7c489e022d5324 Mon Sep 17 00:00:00 2001 From: Koen Konst Date: Thu, 5 Jun 2025 19:54:35 +0200 Subject: [PATCH 05/13] Import moment first before moment-timezone to make sure moment timezone is loaded correctly --- modules/default/calendar/calendar.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 46a4d689af..9993f30560 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -77,7 +77,7 @@ Module.register("calendar", { // Define required scripts. getScripts () { - return ["calendarutils.js", "moment-timezone.js", "moment.js"]; + return ["calendarutils.js", "moment.js", "moment-timezone.js"]; }, // Define required translations. @@ -218,10 +218,6 @@ Module.register("calendar", { // Override dom generator. getDom () { const ONE_SECOND = 1000; // 1,000 milliseconds - const ONE_MINUTE = ONE_SECOND * 60; - const ONE_HOUR = ONE_MINUTE * 60; - const ONE_DAY = ONE_HOUR * 24; - const events = this.createEventList(true); const wrapper = document.createElement("table"); wrapper.className = this.config.tableClass; From 68a80ee1b930c9022b7007efee901e8d7a2ab9cd Mon Sep 17 00:00:00 2001 From: Koen Konst Date: Thu, 5 Jun 2025 22:29:38 +0200 Subject: [PATCH 06/13] Fixed an issue with slicedMultiDay events and datekey generation for exdates and recurrences --- modules/default/calendar/calendar.js | 2 +- .../default/calendar/calendarfetcherutils.js | 50 +++++++------------ 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 9993f30560..975660fee4 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -664,7 +664,7 @@ Module.register("calendar", { event.startDate = midnight.unix(); count += 1; - midnight = midnight.clone().add(1, "day").endOf("day").unix(); // next day + midnight = midnight.clone().add(1, "day").endOf("day"); // next day } // Last day event.title += ` (${count}/${maxCount})`; diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index becf7cec80..ab30a6f25f 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -67,6 +67,15 @@ const CalendarFetcherUtils = { return filter; }, + /** + * Get local timezone. + * This method makes it easier to test if different timezones cause problems by changing this implementation. + * @returns {string} timezone + */ + getLocalTimezone () { + return "America/Los_Angeles"; + }, + /** * This function returns a list of moments for a recurring event. * @param {object} event the current event which is a recurring event @@ -114,7 +123,7 @@ const CalendarFetcherUtils = { // Dates are returned in UTC timezone but with localdatetime because tzid is null. // So we map the date to a moment using the original timezone of the event. - return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(moment.tz.guess(), true))); + return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(CalendarFetcherUtils.getLocalTimezone(), true))); }, /** @@ -202,10 +211,10 @@ const CalendarFetcherUtils = { for (let m in moments) { let curEvent = event; let showRecurrence = true; - let recurringEventStartMoment = moments[m].tz(moment.tz.guess()).clone(); + let recurringEventStartMoment = moments[m].tz(CalendarFetcherUtils.getLocalTimezone()).clone(); let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms"); - let dateKey = CalendarFetcherUtils.getDateKeyFromDate(recurringEventStartMoment.toDate()); + let dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD"); Log.debug("event date dateKey=", dateKey); // For each date that we're checking, it's possible that there is a recurrence override for that one day. @@ -217,14 +226,14 @@ const CalendarFetcherUtils = { curEvent = curEvent.recurrences[dateKey]; // Some event start/end dates don't have timezones if (curEvent.start.tz) { - recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(moment.tz.guess()); + recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(CalendarFetcherUtils.getLocalTimezone()); } else { - recurringEventStartMoment = moment(curEvent.start).tz(moment.tz.guess()); + recurringEventStartMoment = moment(curEvent.start).tz(CalendarFetcherUtils.getLocalTimezone()); } if (curEvent.end.tz) { - recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(moment.tz.guess()); + recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(CalendarFetcherUtils.getLocalTimezone()); } else { - recurringEventEndMoment = moment(curEvent.end).tz(moment.tz.guess()); + recurringEventEndMoment = moment(curEvent.end).tz(CalendarFetcherUtils.getLocalTimezone()); } } else { Log.debug("recurrence key ", dateKey, " doesn't match"); @@ -232,7 +241,7 @@ const CalendarFetcherUtils = { } // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. if (curEvent.exdate !== undefined) { - Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate); + console.log("have datekey=", dateKey, " exdates=", curEvent.exdate); if (curEvent.exdate[dateKey] !== undefined) { // This date is an exception date, which means we should skip it in the recurrence pattern. showRecurrence = false; @@ -335,31 +344,6 @@ const CalendarFetcherUtils = { return newEvents; }, - /** - * get the exdate/recurrence hash key from the date object - * BEFORE calling rrule.between - * @param {Date} date The date of the event - * @returns {string} date key in the format YYYY-MM-DD - */ - getDateKeyFromDate (date) { - // get our runtime timezone offset - let startday = date.getDate(); - Log.debug(" day of month=", (`0${startday}`).slice(-2), ` start time=${date.toString().split(" ")[4].slice(0, 2)}`); - Log.debug("date string= ", date.toString()); - Log.debug("date iso string ", date.toISOString()); - // if the dates are different - if (date.toString().slice(8, 10) < date.toISOString().slice(8, 10)) { - startday = date.toString().slice(8, 10); - Log.debug("< ", startday); - } else { // tostring is more - if (date.toString().slice(8, 10) > date.toISOString().slice(8, 10)) { - startday = date.toISOString().slice(8, 10); - Log.debug("> ", startday); - } - } - return date.toISOString().substring(0, 8) + (`0${startday}`).slice(-2); - }, - /** * Gets the title from the event. * @param {object} event The event object to check. From 0fb0be006de2c1211b023e21adc130557030207d Mon Sep 17 00:00:00 2001 From: Koen Konst Date: Fri, 6 Jun 2025 10:47:48 +0200 Subject: [PATCH 07/13] Fixed a bug in full day recurring events and fixed a broken test that wasn't expecting the correct result since Sydney is +10 but the asserted time was only +2 from Berlin so +4 in total --- CHANGELOG.md | 1 + modules/default/calendar/calendar.js | 13 +++--- .../default/calendar/calendarfetcherutils.js | 7 ++-- tests/electron/modules/calendar_spec.js | 41 +++++++++++-------- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70b05bbad..46e5c51020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ planned for 2025-07-01 - Removed as many of the date conversions as possible - Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly - Added some tests to test the behavior of the refactored methodes to make sure the correct event dates are returned + - Changed the behaviour of the `calendarfetcherutils` so it returns proper unix timestamps in milliseconds instead of microseconds and it will always be in UTC ### Fixed diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 975660fee4..61fda0cd96 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -425,16 +425,15 @@ Module.register("calendar", { // For full day events we use the fullDayEventDateFormat if (event.fullDayEvent) { //subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day - event.endDate -= ONE_SECOND; + eventEndDateMoment.subtract(1, "second"); timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat)); // only show end if requested and allowed and the dates are different if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) { timeWrapper.innerHTML += "-"; timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat)); - } else - if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) { - timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat)); - } + } else if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) { + timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat)); + } } else if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) { // Ongoing and getRelative is set timeWrapper.innerHTML = CalendarUtils.capFirst( @@ -588,7 +587,7 @@ Module.register("calendar", { /** * converts the given timestamp to a moment with a timezone - * @param timestamp timestamp from an event + * @param {number} timestamp timestamp from an event * @returns {moment.Moment} moment with a timezone */ timestampToMoment (timestamp) { @@ -644,7 +643,7 @@ Module.register("calendar", { * if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days, * otherwise, esp. in dateheaders mode it is not clear how long these events are. */ - const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days") + 1; + const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days"); if (this.config.sliceMultiDayEvents && maxCount > 1) { const splitEvents = []; let midnight diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index ab30a6f25f..cf1543d75d 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -1,10 +1,8 @@ /** * @external Moment */ -const path = require("node:path"); const moment = require("moment-timezone"); -const zoneTable = require(path.join(__dirname, "windowsZones.json")); const Log = require("../../../js/logger"); const CalendarFetcherUtils = { @@ -73,7 +71,7 @@ const CalendarFetcherUtils = { * @returns {string} timezone */ getLocalTimezone () { - return "America/Los_Angeles"; + return moment.tz.guess(); }, /** @@ -136,7 +134,8 @@ const CalendarFetcherUtils = { const newEvents = []; const eventDate = function (event, time) { - return CalendarFetcherUtils.isFullDayEvent(event) ? moment.tz(event[time], event[time].tz).startOf("day") : moment.tz(event[time], event[time].tz); + const startMoment = event[time].tz ? moment.tz(event[time], event[time].tz) : moment.tz(event[time], CalendarFetcherUtils.getLocalTimezone()); + return CalendarFetcherUtils.isFullDayEvent(event) ? startMoment.startOf("day") : startMoment; }; Log.debug(`There are ${Object.entries(data).length} calendar entries.`); diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index b819d2dc97..2a9845ae18 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -22,6 +22,19 @@ describe("Calendar module", () => { return await loc.count(); }; + /** + * Use this for debugging broken tests, it will console log the text of the calendar module + * @returns {Promise} + */ + const logAllText = async () => { + expect(global.page).not.toBeNull(); + const loc = await global.page.locator(".calendar .event"); + const elem = loc.first(); + await elem.waitFor(); + expect(elem).not.toBeNull(); + console.log(await loc.allInnerTexts()); + }; + const first = 0; const second = 1; const third = 2; @@ -153,19 +166,6 @@ describe("Calendar module", () => { * RRULE TESTS: * Add any tests that check rrule functionality here. */ - describe("sliceMultiDayEvents", () => { - it("Issue #3452 split multiday in Europe", async () => { - await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", [], "Europe/Berlin"); - expect(global.page).not.toBeNull(); - const loc = await global.page.locator(".calendar .event"); - const elem = loc.first(); - await elem.waitFor(); - expect(elem).not.toBeNull(); - const cnt = await loc.count(); - expect(cnt).toBe(6); - }); - }); - describe("sliceMultiDayEvents direct count", () => { it("Issue #3452 split multiday in Europe", async () => { await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", [], "Europe/Berlin"); @@ -197,21 +197,30 @@ describe("Calendar module", () => { describe("berlin late in day event moved, viewed from berlin", () => { it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Europe/Berlin"); - await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true); + await expect(doTestCount()).resolves.toBe(3); + await expect(doTestTableContent(".calendar .event", ".time", "22nd.Oct, 23:00-00:00", first)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "23rd.Oct, 23:00-00:00", second)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", third)).resolves.toBe(true); }); }); describe("berlin late in day event moved, viewed from sydney", () => { it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Australia/Sydney"); - await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 01:00-02:00", last)).resolves.toBe(true); + await expect(doTestCount()).resolves.toBe(3); + await expect(doTestTableContent(".calendar .event", ".time", "23rd.Oct, 08:00-09:00", first)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 08:00-09:00", second)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 08:00-09:00", third)).resolves.toBe(true); }); }); describe("berlin late in day event moved, viewed from chicago", () => { it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "America/Chicago"); - await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 16:00-17:00", last)).resolves.toBe(true); + await expect(doTestCount()).resolves.toBe(3); + await expect(doTestTableContent(".calendar .event", ".time", "22nd.Oct, 16:00-17:00", first)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "23rd.Oct, 16:00-17:00", second)).resolves.toBe(true); + await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 16:00-17:00", third)).resolves.toBe(true); }); }); From 51550a4056cb23d1b7fd67d44729763eb1db5f7e Mon Sep 17 00:00:00 2001 From: Koen Konst Date: Fri, 6 Jun 2025 11:25:27 +0200 Subject: [PATCH 08/13] Fixed a small issue where we didn't search back enough for currently running events, now subtracted the event duration from the searchFromDate --- modules/default/calendar/calendarfetcherutils.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index cf1543d75d..77959cf770 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -79,9 +79,10 @@ const CalendarFetcherUtils = { * @param {object} event the current event which is a recurring event * @param {moment.Moment} pastLocalMoment The past date to search for recurring events * @param {moment.Moment} futureLocalMoment The future date to search for recurring events + * @param {number} durationInMs the duration of the event, this is used to take into account currently running events * @returns {moment.Moment[]} All moments for the recurring event */ - getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment) { + getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment, durationInMs) { const rule = event.rrule; // can cause problems with e.g. birthdays before 1900 @@ -90,7 +91,8 @@ const CalendarFetcherUtils = { rule.options.dtstart.setYear(1900); } - let searchFromDate = pastLocalMoment.clone().subtract(1, "days").toDate(); + // subtract the duration of this event to find events in the past that are currently still running and should therefor be displayed. + let searchFromDate = pastLocalMoment.clone().subtract(durationInMs, "milliseconds").toDate(); let searchToDate = futureLocalMoment.clone().add(1, "days").toDate(); Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`); @@ -203,7 +205,7 @@ const CalendarFetcherUtils = { // TODO This should be a seperate function. if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) { // Recurring event. - let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment); + let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs); // Loop through the set of moment entries to see which recurrences should be added to our event list. // TODO This should create an event per moment so we can change anything we want. From 7e2690f37f42b437c66ab8d346ec845343088aa1 Mon Sep 17 00:00:00 2001 From: Koen Konst Date: Fri, 6 Jun 2025 12:01:30 +0200 Subject: [PATCH 09/13] Made the calendarfetcherutil backwards compatible again by using format(x) so the timestamps returned will be in milliseconds again instead of the unix timestamp in seconds, changed calendar.js accordingly --- CHANGELOG.md | 1 - modules/default/calendar/calendar.js | 6 +-- .../default/calendar/calendarfetcherutils.js | 13 ++++--- .../calendar/calendar_fetcher_utils_spec.js | 37 ++++++++++++++++--- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e5c51020..e70b05bbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,6 @@ planned for 2025-07-01 - Removed as many of the date conversions as possible - Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly - Added some tests to test the behavior of the refactored methodes to make sure the correct event dates are returned - - Changed the behaviour of the `calendarfetcherutils` so it returns proper unix timestamps in milliseconds instead of microseconds and it will always be in UTC ### Fixed diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 61fda0cd96..a7aad9b38b 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -591,7 +591,7 @@ Module.register("calendar", { * @returns {moment.Moment} moment with a timezone */ timestampToMoment (timestamp) { - return moment.unix(timestamp).tz(moment.tz.guess()); + return moment(timestamp, "x").tz(moment.tz.guess()); }, /** @@ -657,11 +657,11 @@ Module.register("calendar", { const thisEvent = JSON.parse(JSON.stringify(event)); // clone object thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d"); thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d"); - thisEvent.endDate = midnight.clone().subtract(1, "day").unix(); + thisEvent.endDate = midnight.clone().subtract(1, "day").format("x"); thisEvent.title += ` (${count}/${maxCount})`; splitEvents.push(thisEvent); - event.startDate = midnight.unix(); + event.startDate = midnight.format("x"); count += 1; midnight = midnight.clone().add(1, "day").endOf("day"); // next day } diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index 77959cf770..a58ccbc1e7 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -91,8 +91,9 @@ const CalendarFetcherUtils = { rule.options.dtstart.setYear(1900); } - // subtract the duration of this event to find events in the past that are currently still running and should therefor be displayed. - let searchFromDate = pastLocalMoment.clone().subtract(durationInMs, "milliseconds").toDate(); + // subtract the max of the duration of this event or 1 day to find events in the past that are currently still running and should therefor be displayed. + const oneDayInMs = 24 * 60 * 60000; + let searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate(); let searchToDate = futureLocalMoment.clone().add(1, "days").toDate(); Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`); @@ -269,8 +270,8 @@ const CalendarFetcherUtils = { Log.debug(`saving event: ${recurrenceTitle}`); newEvents.push({ title: recurrenceTitle, - startDate: recurringEventStartMoment.unix(), - endDate: recurringEventEndMoment.unix(), + startDate: recurringEventStartMoment.format("x"), + endDate: recurringEventEndMoment.format("x"), fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), recurringEvent: true, class: event.class, @@ -324,8 +325,8 @@ const CalendarFetcherUtils = { // Every thing is good. Add it to the list. newEvents.push({ title: title, - startDate: eventStartMoment.unix(), - endDate: eventEndMoment.unix(), + startDate: eventStartMoment.format("x"), + endDate: eventEndMoment.format("x"), fullDayEvent: fullDayEvent, recurringEvent: false, class: event.class, diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index 14744d8c4b..ec6b8eff93 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -72,19 +72,21 @@ END:VEVENT`); const filteredEvents = CalendarFetcherUtils.filterEvents(data, defaultConfig); - const januaryFirst = filteredEvents.filter((event) => moment.unix(event.startDate).format("MM-DD") === "01-01"); - const julyFirst = filteredEvents.filter((event) => moment.unix(event.startDate).format("MM-DD") === "07-01"); + console.log(filteredEvents); - let januaryMoment = moment(`${moment.unix(januaryFirst[0].startDate).format("YYYY")}-01-01T09:00:00`) + const januaryFirst = filteredEvents.filter((event) => moment(event.startDate, "x").format("MM-DD") === "01-01"); + const julyFirst = filteredEvents.filter((event) => moment(event.startDate, "x").format("MM-DD") === "07-01"); + + let januaryMoment = moment(`${moment(januaryFirst[0].startDate, "x").format("YYYY")}-01-01T09:00:00`) .tz("Europe/Amsterdam", true) // Convert to Europe/Amsterdam timezone (see event ical) but keep 9 o'clock .tz(moment.tz.guess()); // Convert to guessed timezone as that is used in the filterEvents - let julyMoment = moment(`${moment.unix(julyFirst[0].startDate).format("YYYY")}-07-01T09:00:00`) + let julyMoment = moment(`${moment(julyFirst[0].startDate, "x").format("YYYY")}-07-01T09:00:00`) .tz("Europe/Amsterdam", true) // Convert to Europe/Amsterdam timezone (see event ical) but keep 9 o'clock .tz(moment.tz.guess()); // Convert to guessed timezone as that is used in the filterEvents - expect(januaryFirst[0].startDate).toEqual(januaryMoment.unix()); - expect(julyFirst[0].startDate).toEqual(julyMoment.unix()); + expect(januaryFirst[0].startDate).toEqual(januaryMoment.format("x")); + expect(julyFirst[0].startDate).toEqual(julyMoment.format("x")); }); it("should return the correct moments based on the timezone given", () => { @@ -112,5 +114,28 @@ END:VEVENT`); expect(januaryFirst[0].toISOString(true)).toContain("09:00:00.000+01:00"); expect(julyFirst[0].toISOString(true)).toContain("09:00:00.000+02:00"); }); + + it("debug", () => { + const data = ical.parseICS(`BEGIN:VEVENT +DTSTART;TZID=America/New_York:20240918T183000 +DTEND;TZID=America/New_York:20240918T203000 +RRULE:FREQ=WEEKLY;BYDAY=WE +EXDATE;TZID=America/New_York:20241127T183000 +EXDATE;TZID=America/New_York:20241225T183000 +DTSTAMP:20250122T045443Z +UID:_@google.com +CREATED:20240916T131843Z +LAST-MODIFIED:20241222T235014Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Derby +TRANSP:OPAQUE +END:VEVENT`); + + const filteredEvents = CalendarFetcherUtils.filterEvents(data, defaultConfig); + + console.log(filteredEvents); + + }); }); }); From 49011743dc7b7e291d8b1edeea7b0a5e76cbfc1e Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:01:35 +0200 Subject: [PATCH 10/13] Fix typo and add PR id --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70b05bbad..7202330346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,10 +34,10 @@ planned for 2025-07-01 - [refactor] Replace `ansis` with built-in function `util.styleText` (#3793) - [core] Integrate stuff from `vendor` and `fonts` folders into main `package.json`, simplifies install and maintaining dependencies (#3795) - [l10n] Complete translations (with the help of translation tools) (#3794) -- [refactor] Refactored `calendarfetcherutils` in Calendar module to handle timezones better +- [refactor] Refactored `calendarfetcherutils` in Calendar module to handle timezones better (#3806) - Removed as many of the date conversions as possible - Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly - - Added some tests to test the behavior of the refactored methodes to make sure the correct event dates are returned + - Added some tests to test the behavior of the refactored methods to make sure the correct event dates are returned ### Fixed From 25e02b472b4911b738e402c13804412a8d4d5cc0 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:22:24 +0200 Subject: [PATCH 11/13] Update cspell.config.json --- cspell.config.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cspell.config.json b/cspell.config.json index e681059f84..54a78a9383 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -52,6 +52,8 @@ "dkallen", "drivelist", "DTEND", + "DTSTAMP", + "DTSTART", "Duffman", "earlman", "easyas", @@ -107,6 +109,7 @@ "jsonlint", "jupadin", "kaennchenstruggle", + "Kalenderwoche", "kenzal", "Keyport", "khassel", From f52a8c74de4086f46e89b4b3c703bad57f04965b Mon Sep 17 00:00:00 2001 From: plebcity Date: Fri, 6 Jun 2025 22:31:55 +0200 Subject: [PATCH 12/13] remove unused unit test for calendar module --- .../calendar/calendar_fetcher_utils_spec.js | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index ec6b8eff93..909055e75d 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -114,28 +114,5 @@ END:VEVENT`); expect(januaryFirst[0].toISOString(true)).toContain("09:00:00.000+01:00"); expect(julyFirst[0].toISOString(true)).toContain("09:00:00.000+02:00"); }); - - it("debug", () => { - const data = ical.parseICS(`BEGIN:VEVENT -DTSTART;TZID=America/New_York:20240918T183000 -DTEND;TZID=America/New_York:20240918T203000 -RRULE:FREQ=WEEKLY;BYDAY=WE -EXDATE;TZID=America/New_York:20241127T183000 -EXDATE;TZID=America/New_York:20241225T183000 -DTSTAMP:20250122T045443Z -UID:_@google.com -CREATED:20240916T131843Z -LAST-MODIFIED:20241222T235014Z -SEQUENCE:0 -STATUS:CONFIRMED -SUMMARY:Derby -TRANSP:OPAQUE -END:VEVENT`); - - const filteredEvents = CalendarFetcherUtils.filterEvents(data, defaultConfig); - - console.log(filteredEvents); - - }); }); }); From e1bbbea336ad2bbf79353486c21d757b76a9c878 Mon Sep 17 00:00:00 2001 From: plebcity Date: Sat, 7 Jun 2025 11:06:22 +0200 Subject: [PATCH 13/13] removed console.log statements which were used for debugging --- modules/default/calendar/calendarfetcherutils.js | 2 +- .../modules/default/calendar/calendar_fetcher_utils_spec.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js index a58ccbc1e7..880e68b5cf 100644 --- a/modules/default/calendar/calendarfetcherutils.js +++ b/modules/default/calendar/calendarfetcherutils.js @@ -243,7 +243,7 @@ const CalendarFetcherUtils = { } // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule. if (curEvent.exdate !== undefined) { - console.log("have datekey=", dateKey, " exdates=", curEvent.exdate); + Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate); if (curEvent.exdate[dateKey] !== undefined) { // This date is an exception date, which means we should skip it in the recurrence pattern. showRecurrence = false; diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index 909055e75d..db8861807d 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -72,8 +72,6 @@ END:VEVENT`); const filteredEvents = CalendarFetcherUtils.filterEvents(data, defaultConfig); - console.log(filteredEvents); - const januaryFirst = filteredEvents.filter((event) => moment(event.startDate, "x").format("MM-DD") === "01-01"); const julyFirst = filteredEvents.filter((event) => moment(event.startDate, "x").format("MM-DD") === "07-01");