From 12db2db6f4b80ed6b1ff380d6ca2d1411745854a Mon Sep 17 00:00:00 2001 From: Junping Luo <53324036+JacE070@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:31:24 -0800 Subject: [PATCH 1/2] Fix the duplicate calendar events bug --- apps/antalmanac/src/lib/download.ts | 75 +++++++++---------- .../src/stores/calendarizeHelpers.ts | 14 +++- .../__snapshots__/download-ics.test.ts.snap | 2 +- apps/antalmanac/tests/download-ics.test.ts | 33 ++++++++ 4 files changed, 83 insertions(+), 41 deletions(-) diff --git a/apps/antalmanac/src/lib/download.ts b/apps/antalmanac/src/lib/download.ts index abcbade2a..9dba724bf 100644 --- a/apps/antalmanac/src/lib/download.ts +++ b/apps/antalmanac/src/lib/download.ts @@ -247,7 +247,9 @@ export function getRRule(bydays: string[], quarter: string) { return `FREQ=WEEKLY;BYDAY=${bydays.toString()};INTERVAL=1;COUNT=${count}`; } -export function getEventsFromCourses(events = AppStore.getEventsInCalendar()): EventAttributes[] { +export function getEventsFromCourses( + events = [...AppStore.getEventsInCalendar(), ...AppStore.getFinalEventsInCalendar()] +): EventAttributes[] { const calendarEvents = events.flatMap((event) => { if (event.isCustomEvent) { // FIXME: We don't have a way to get the term for custom events, @@ -281,45 +283,42 @@ export function getEventsFromCourses(events = AppStore.getEventsInCalendar()): E if (location.days === undefined) return null; const days = getByDays(location.days); - const classStartDate = getClassStartDate(term, days); - - const [firstClassStart, firstClassEnd] = getFirstClass( - classStartDate, - { hour: start.getHours(), minute: start.getMinutes() }, - { hour: end.getHours(), minute: end.getMinutes() } - ); - - const rrule = getRRule(days, getQuarter(term)); - - // Add VEvent to events array. - return { - productId: 'antalmanac/ics', - startOutputType: 'local' as const, - endOutputType: 'local' as const, - title: `${title} ${sectionType}`, - description: `${courseTitle}\nTaught by ${instructors.join('/')}`, - location: `${location.building} ${location.room}`, - start: firstClassStart, - end: firstClassEnd, - recurrenceRule: rrule, - }; + if (sectionType === 'Fin') { + return { + productId: 'antalmanac/ics', + startOutputType: 'local' as const, + endOutputType: 'local' as const, + title: `${title} Final Exam`, + description: `Final Exam for ${courseTitle}`, + start: getExamTime(finalExam, getYear(term))[0]!, + end: getExamTime(finalExam, getYear(term))[1]!, + }; + } else { + const classStartDate = getClassStartDate(term, days); + + const [firstClassStart, firstClassEnd] = getFirstClass( + classStartDate, + { hour: start.getHours(), minute: start.getMinutes() }, + { hour: end.getHours(), minute: end.getMinutes() } + ); + + const rrule = getRRule(days, getQuarter(term)); + + // Add VEvent to events array. + return { + productId: 'antalmanac/ics', + startOutputType: 'local' as const, + endOutputType: 'local' as const, + title: `${title} ${sectionType}`, + description: `${courseTitle}\nTaught by ${instructors.join('/')}`, + location: `${location.building} ${location.room}`, + start: firstClassStart, + end: firstClassEnd, + recurrenceRule: rrule, + }; + } }) .filter(notNull); - - // Add final to events. - if (finalExam.examStatus === 'SCHEDULED_FINAL') { - if (finalExam.startTime && finalExam.endTime) { - courseEvents.push({ - productId: 'antalmanac/ics', - startOutputType: 'local' as const, - endOutputType: 'local' as const, - title: `${title} Final Exam`, - description: `Final Exam for ${sectionType} ${courseTitle}`, - start: getExamTime(finalExam, getYear(term))[0]!, - end: getExamTime(finalExam, getYear(term))[1]!, - }); - } - } return courseEvents; } }); diff --git a/apps/antalmanac/src/stores/calendarizeHelpers.ts b/apps/antalmanac/src/stores/calendarizeHelpers.ts index 8d36dcbe1..84397555e 100644 --- a/apps/antalmanac/src/stores/calendarizeHelpers.ts +++ b/apps/antalmanac/src/stores/calendarizeHelpers.ts @@ -49,7 +49,10 @@ export function calendarizeCourseEvents(currentCourses: ScheduleCourse[] = []): title: `${course.deptCode} ${course.courseNumber}`, courseTitle: course.courseTitle, locations: meeting.bldg.map(getLocation).map((location: Location) => { - return { ...location, days: meeting.days === null ? undefined : meeting.days }; + return { + ...location, + days: meeting.days === null ? undefined : COURSE_WEEK_DAYS[dayIndex], + }; }), showLocationInfo: false, instructors: course.section.instructors, @@ -99,12 +102,19 @@ export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): Course */ const dayIndicesOcurring = weekdaysOccurring.map((day, index) => (day ? index : undefined)).filter(notNull); + const locationsWithNoDays = bldg ? bldg.map(getLocation) : course.section.meetings[0].bldg.map(getLocation); + return dayIndicesOcurring.map((dayIndex) => ({ color: course.section.color, term: course.term, title: `${course.deptCode} ${course.courseNumber}`, courseTitle: course.courseTitle, - locations: bldg ? bldg.map(getLocation) : course.section.meetings[0].bldg.map(getLocation), + locations: locationsWithNoDays.map((location: Location) => { + return { + ...location, + days: COURSE_WEEK_DAYS[dayIndex], + }; + }), showLocationInfo: true, instructors: course.section.instructors, sectionCode: course.section.sectionCode, diff --git a/apps/antalmanac/tests/__snapshots__/download-ics.test.ts.snap b/apps/antalmanac/tests/__snapshots__/download-ics.test.ts.snap index 32bab2a46..d5672bc77 100644 --- a/apps/antalmanac/tests/__snapshots__/download-ics.test.ts.snap +++ b/apps/antalmanac/tests/__snapshots__/download-ics.test.ts.snap @@ -27,7 +27,7 @@ Taught by placeholderInstructor1/placeholderInstructor2", "title": "placeholderDeptCode placeholderCourseNumber placeholderSectionType", }, { - "description": "Final Exam for placeholderSectionType placeholderCourseTitle", + "description": "Final Exam for placeholderCourseTitle", "end": [ 2023, 3, diff --git a/apps/antalmanac/tests/download-ics.test.ts b/apps/antalmanac/tests/download-ics.test.ts index f07b36289..eb2b0d801 100644 --- a/apps/antalmanac/tests/download-ics.test.ts +++ b/apps/antalmanac/tests/download-ics.test.ts @@ -15,6 +15,8 @@ describe('download-ics', () => { title: 'placeholderDeptCode placeholderCourseNumber', locations: [{ building: 'placeholderLocation', room: 'placeholderRoom', days: 'MWF' }], showLocationInfo: true, + // We don't use finalExam anymore for calendar file export, + // instead, FinalExamEvent is used finalExam: { examStatus: 'SCHEDULED_FINAL', dayOfWeek: 'Mon', @@ -37,6 +39,36 @@ describe('download-ics', () => { sectionType: 'placeholderSectionType', term: '2023 Fall', // Cannot be a random placeholder; it has to be in `quarterStartDates` otherwise it'll be undefined }, + // FinalExamEvent + { + color: 'placeholderColor', + start: new Date(2023, 9, 29, 1, 2), + end: new Date(2023, 9, 29, 3, 4), + title: 'placeholderDeptCode placeholderCourseNumber', + locations: [{ building: 'placeholderLocation', room: 'placeholderRoom', days: 'MWF' }], + showLocationInfo: true, + finalExam: { + examStatus: 'SCHEDULED_FINAL', + dayOfWeek: 'Mon', + month: 2, + day: 3, + startTime: { + hour: 1, + minute: 2, + }, + endTime: { + hour: 3, + minute: 4, + }, + locations: [{ building: 'placeholderFinalLocation', room: 'placeholderFinalRoom' }], + }, + courseTitle: 'placeholderCourseTitle', + instructors: ['placeholderInstructor1', 'placeholderInstructor2'], + isCustomEvent: false, + sectionCode: 'placeholderSectionCode', + sectionType: 'Fin', + term: '2023 Fall', // Cannot be a random placeholder; it has to be in `quarterStartDates` otherwise it'll be undefined + }, // CustomEvent { color: 'placeholderColor', @@ -46,6 +78,7 @@ describe('download-ics', () => { customEventID: 123, isCustomEvent: true, days: ['M', 'W', 'F'], + building: 'placeholderCustomEventBuilding', }, ]; From 9efca4625951a16f9c8545af95afcfaefb93a999 Mon Sep 17 00:00:00 2001 From: Junping Luo <53324036+JacE070@users.noreply.github.com> Date: Sun, 3 Dec 2023 22:34:15 -0800 Subject: [PATCH 2/2] Resolve comments --- apps/antalmanac/src/lib/download.ts | 24 ++++++++++++------- apps/antalmanac/src/stores/AppStore.ts | 4 ++++ .../src/stores/calendarizeHelpers.ts | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/antalmanac/src/lib/download.ts b/apps/antalmanac/src/lib/download.ts index 9dba724bf..8a4cd8649 100644 --- a/apps/antalmanac/src/lib/download.ts +++ b/apps/antalmanac/src/lib/download.ts @@ -45,6 +45,10 @@ export const vTimeZoneSection = 'END:VTIMEZONE\n' + 'BEGIN:VEVENT'; +export const CALENDAR_ID = 'antalmanac/ics'; + +export const CALENDAR_OUTPUT = 'local' as const; + /** * @example [YEAR, MONTH, DAY, HOUR, MINUTE] */ @@ -247,9 +251,7 @@ export function getRRule(bydays: string[], quarter: string) { return `FREQ=WEEKLY;BYDAY=${bydays.toString()};INTERVAL=1;COUNT=${count}`; } -export function getEventsFromCourses( - events = [...AppStore.getEventsInCalendar(), ...AppStore.getFinalEventsInCalendar()] -): EventAttributes[] { +export function getEventsFromCourses(events = AppStore.getEventsWithFinalsInCalendar()): EventAttributes[] { const calendarEvents = events.flatMap((event) => { if (event.isCustomEvent) { // FIXME: We don't have a way to get the term for custom events, @@ -280,18 +282,22 @@ export function getEventsFromCourses( const { term, title, courseTitle, instructors, sectionType, start, end, finalExam } = event; const courseEvents: EventAttributes[] = event.locations .map((location) => { - if (location.days === undefined) return null; + if (location.days === undefined) { + return null; + } const days = getByDays(location.days); + const [finalStart, finalEnd] = getExamTime(finalExam, getYear(term)); + if (sectionType === 'Fin') { return { - productId: 'antalmanac/ics', - startOutputType: 'local' as const, - endOutputType: 'local' as const, + productId: CALENDAR_ID, + startOutputType: CALENDAR_OUTPUT, + endOutputType: CALENDAR_OUTPUT, title: `${title} Final Exam`, description: `Final Exam for ${courseTitle}`, - start: getExamTime(finalExam, getYear(term))[0]!, - end: getExamTime(finalExam, getYear(term))[1]!, + start: finalStart!, + end: finalEnd!, }; } else { const classStartDate = getClassStartDate(term, days); diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index 8be07c600..77ae43f0f 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -90,6 +90,10 @@ class AppStore extends EventEmitter { return this.schedule.getCalendarizedEvents(); } + getEventsWithFinalsInCalendar() { + return [...this.schedule.getCalendarizedEvents(), ...this.schedule.getCalendarizedFinals()]; + } + getCourseEventsInCalendar() { return this.schedule.getCalendarizedCourseEvents(); } diff --git a/apps/antalmanac/src/stores/calendarizeHelpers.ts b/apps/antalmanac/src/stores/calendarizeHelpers.ts index 84397555e..282247aad 100644 --- a/apps/antalmanac/src/stores/calendarizeHelpers.ts +++ b/apps/antalmanac/src/stores/calendarizeHelpers.ts @@ -51,7 +51,7 @@ export function calendarizeCourseEvents(currentCourses: ScheduleCourse[] = []): locations: meeting.bldg.map(getLocation).map((location: Location) => { return { ...location, - days: meeting.days === null ? undefined : COURSE_WEEK_DAYS[dayIndex], + ...(meeting.days && { days: COURSE_WEEK_DAYS[dayIndex] }), }; }), showLocationInfo: false,