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 =>