From 2d92dd47f00a44b7d48e92a8ffba94480e4e73f9 Mon Sep 17 00:00:00 2001 From: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com> Date: Sun, 8 Jun 2025 21:10:05 -0700 Subject: [PATCH] feat: support summer grades, fix summer course parser (#596) * feat: support summer grades, fix summer course parser * chore: lint * docs: mention summer terms in Course::number description * feat: Course::getNumberWithoutTerm, strip summer term indicator when displaying grades --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> --- src/shared/types/Course.ts | 59 ++++++++++++++++++- src/shared/types/tests/Course.test.ts | 57 ++++++++++++++++++ .../components/SortableList.stories.tsx | 7 +-- src/views/components/PopupMain.tsx | 9 ++- .../GradeDistribution.tsx | 7 ++- src/views/lib/CourseCatalogScraper.test.ts | 34 +++++++++++ src/views/lib/CourseCatalogScraper.ts | 33 ++++++++--- src/views/lib/database/queryDistribution.ts | 18 ++++-- 8 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 src/shared/types/tests/Course.test.ts create mode 100644 src/views/lib/CourseCatalogScraper.test.ts diff --git a/src/shared/types/Course.ts b/src/shared/types/Course.ts index 0e47a957..86d4806c 100644 --- a/src/shared/types/Course.ts +++ b/src/shared/types/Course.ts @@ -44,7 +44,12 @@ export type Semester = { export class Course { /** Every course has a uniqueId within UT's registrar system corresponding to each course section */ uniqueId!: number; - /** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */ + /** + * This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H. + * UT prefixes summer courses with f, s, n, or w: + * [f]irst term, [s]econd term, [n]ine week term, [w]hole term. + * So, the first term of PSY 301 over the summer would be 'f301' + */ number!: string; /** The full name of the course, i.e. CS 314 Data Structures and Algorithms */ fullName!: string; @@ -91,6 +96,46 @@ export class Course { } this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500); this.core = course.core ?? []; + if (course.semester.season === 'Summer') { + // A bug from and old version put the summer term in the course, + // so we need to handle that case + const { department, number } = Course.cleanSummerTerm(course.department, course.number); + this.department = department; + this.number = number; + } + } + + /** + * Due to a bug in an older version, the summer term was included in the course department code, + * instead of the course number. + * UT prefixes summer courses with f, s, n, or w: + * [f]irst term, [s]econd term, [n]ine week term, [w]hole term + * + * @param department - The course department code, like 'C S' + * @param number - The course number, like '314H' + * @returns The properly formatted department and course number + * @example + * ```ts + * cleanSummerTerm('C S', '314H') // { department: 'C S', number: '314H' } + * cleanSummerTerm('P R', 'f378') // { department: 'P R', number: 'f378' } + * cleanSummerTerm('P R f', '378') // { department: 'P R', number: 'f378' } + * cleanSummerTerm('P S', 'n303') // { department: 'P S', number: 'n303' } + * cleanSummerTerm('P S n', '303') // { department: 'P S', number: 'n303' } + * ``` + */ + static cleanSummerTerm(department: string, number: string): { department: string; number: string } { + // UT prefixes summer courses with f, s, n, or w: + // [f]irst term, [s]econd term, [n]ine week term, [w]hole term + const summerTerm = department.match(/[fsnw]$/); + + if (!summerTerm) { + return { department, number }; + } + + return { + department: department.slice(0, -1).trim(), + number: summerTerm[0] + number, + }; } /** @@ -111,6 +156,18 @@ export class Course { return conflicts; } + + /** + * @returns The course number without the summer term + * @example + * ```ts + * const c = new Course({ number: 'f301', ... }); + * c.getNumberWithoutTerm() // '301' + * ``` + */ + getNumberWithoutTerm(): string { + return this.number.replace(/^\D/, ''); // Remove nondigit at start, if it exists + } } /** diff --git a/src/shared/types/tests/Course.test.ts b/src/shared/types/tests/Course.test.ts new file mode 100644 index 00000000..efeed0a1 --- /dev/null +++ b/src/shared/types/tests/Course.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; + +import { Course } from '../Course'; + +describe('Course::cleanSummerTerm', () => { + it("shouldn't affect already cleaned summer terms", () => { + const inputs = [ + ['C S', '314H'], + ['P R', 'f378'], + ['P S', 'f303'], + ['WGS', 's301'], + ['S W', 'n360K'], + ['GOV', 'w312L'], + ['J', 's311F'], + ['J S', '311F'], + ] as const; + const expected = [ + { department: 'C S', number: '314H' }, + { department: 'P R', number: 'f378' }, + { department: 'P S', number: 'f303' }, + { department: 'WGS', number: 's301' }, + { department: 'S W', number: 'n360K' }, + { department: 'GOV', number: 'w312L' }, + { department: 'J', number: 's311F' }, + { department: 'J S', number: '311F' }, + ]; + + const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1])); + + expect(results).toEqual(expected); + }); + + it('should move summer term indicator to course number', () => { + const inputs = [ + ['P R f', '378'], + ['P S f', '303'], + ['WGS s', '301'], + ['S W n', '360K'], + ['GOV w', '312L'], + ['J s', '311F'], + ['J S', '311F'], + ] as const; + const expected = [ + { department: 'P R', number: 'f378' }, + { department: 'P S', number: 'f303' }, + { department: 'WGS', number: 's301' }, + { department: 'S W', number: 'n360K' }, + { department: 'GOV', number: 'w312L' }, + { department: 'J', number: 's311F' }, + { department: 'J S', number: '311F' }, + ]; + + const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1])); + + expect(results).toEqual(expected); + }); +}); diff --git a/src/stories/components/SortableList.stories.tsx b/src/stories/components/SortableList.stories.tsx index 9d96b618..ec1fad0c 100644 --- a/src/stories/components/SortableList.stories.tsx +++ b/src/stories/components/SortableList.stories.tsx @@ -73,7 +73,7 @@ const generateCourses = (count: number): Course[] => { const exampleCourses = generateCourses(numberOfCourses); -type CourseWithId = Course & BaseItem; +type CourseWithId = { course: Course } & BaseItem; const meta = { title: 'Components/Common/SortableList', @@ -91,11 +91,10 @@ export const Default: Story = { args: { draggables: exampleCourses.map(course => ({ id: course.uniqueId, - ...course, - getConflicts: course.getConflicts, + course, })), onChange: () => {}, - renderItem: course => , + renderItem: ({ id, course }) => , }, render: args => (
diff --git a/src/views/components/PopupMain.tsx b/src/views/components/PopupMain.tsx index 1ee5864e..53d8b24f 100644 --- a/src/views/components/PopupMain.tsx +++ b/src/views/components/PopupMain.tsx @@ -155,15 +155,14 @@ export default function PopupMain(): JSX.Element { ({ id: course.uniqueId, - ...course, - getConflicts: course.getConflicts, + course, }))} onChange={reordered => { - activeSchedule.courses = reordered.map(({ id: _id, ...course }) => course); + activeSchedule.courses = reordered.map(({ course }) => course); replaceSchedule(getActiveSchedule(), activeSchedule); }} - renderItem={course => ( - + renderItem={({ id, course }) => ( + )} /> )} diff --git a/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx b/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx index ff8ccfb9..2548107c 100644 --- a/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx +++ b/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx @@ -215,7 +215,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J options={{ ...chartOptions, title: { - text: `There is currently no grade distribution data for ${course.department} ${course.number}`, + text: `There is currently no grade distribution data for ${course.department} ${course.getNumberWithoutTerm()}`, }, tooltip: { enabled: false }, }} @@ -228,7 +228,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J Grade Distribution for{' '} - {course.department} {course.number} + {course.department} {course.getNumberWithoutTerm()}