Compare commits
6 Commits
v2.2.1
...
derek/coll
| Author | SHA1 | Date | |
|---|---|---|---|
| a423c6ed4e | |||
|
|
7b401add15 | ||
|
|
2d92dd47f0 | ||
|
|
eb8141ee8c | ||
|
|
2a50f5580d | ||
|
|
65bfb1d129 |
@@ -7,3 +7,6 @@ insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
[*.nix]
|
||||
indent_size = 2
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -211,3 +211,5 @@ sketch
|
||||
package-lock.json
|
||||
storybook-static/
|
||||
package/
|
||||
|
||||
.direnv/
|
||||
|
||||
10
README.md
10
README.md
@@ -26,8 +26,9 @@
|
||||
## Toolchain
|
||||
|
||||
- React v20.9.0 (LTS)
|
||||
- TypeScript
|
||||
- Vite 5
|
||||
- TypeScript v5.x
|
||||
- Vite v5.x
|
||||
- pnpm v10.x
|
||||
- UnoCSS
|
||||
- ESLint
|
||||
- Prettier
|
||||
@@ -184,8 +185,9 @@ We maintain a strict code of conduct. By contributing, you agree to adhere to th
|
||||
Special thanks to the developers and contributors behind these amazing tools and libraries:
|
||||
|
||||
- React v20.9.0 (LTS)
|
||||
- TypeScript
|
||||
- Vite 5
|
||||
- TypeScript v5.x
|
||||
- Vite v5.x
|
||||
- pnpm v10.x
|
||||
- UnoCSS
|
||||
- ESLint
|
||||
- Prettier
|
||||
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1744932701,
|
||||
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
32
flake.nix
Normal file
32
flake.nix
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
inputs.flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = (import (inputs.nixpkgs) { inherit system; });
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixfmt-rfc-style;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_20 # v20.19.0
|
||||
pnpm_10 # v10.8.1
|
||||
just
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "UTRP Nix Flake Environment Loaded"
|
||||
echo "Node: $(node --version)"
|
||||
echo "pnpm: $(pnpm --version)"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,9 @@ export default async function createSchedule(scheduleName: string) {
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
|
||||
// Automatically switch to the new schedule
|
||||
await UserScheduleStore.set('activeIndex', schedules.length - 1);
|
||||
|
||||
// If there is only one schedule, set the active index to the new schedule
|
||||
if (schedules.length <= 1) {
|
||||
await UserScheduleStore.set('activeIndex', 0);
|
||||
|
||||
@@ -31,5 +31,9 @@ export default async function duplicateSchedule(scheduleId: string): Promise<str
|
||||
} satisfies typeof schedule);
|
||||
|
||||
await UserScheduleStore.set('schedules', schedules);
|
||||
|
||||
// Automatically switch to the duplicated schedule
|
||||
await UserScheduleStore.set('activeIndex', scheduleIndex + 1);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
57
src/shared/types/tests/Course.test.ts
Normal file
57
src/shared/types/tests/Course.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 => <PopupCourseBlock key={course.id} course={course} colors={course.colors} />,
|
||||
renderItem: ({ id, course }) => <PopupCourseBlock key={id} course={course} colors={course.colors} />,
|
||||
},
|
||||
render: args => (
|
||||
<div className='h-3xl w-3xl transform-none'>
|
||||
|
||||
@@ -155,15 +155,14 @@ export default function PopupMain(): JSX.Element {
|
||||
<SortableList
|
||||
draggables={activeSchedule.courses.map(course => ({
|
||||
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 => (
|
||||
<PopupCourseBlock key={course.id} course={course} colors={course.colors} />
|
||||
renderItem={({ id, course }) => (
|
||||
<PopupCourseBlock key={id} course={course} colors={course.colors} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
||||
const [activeSchedule] = useSchedules();
|
||||
|
||||
return (
|
||||
<div className='border border-ut-offwhite/50 rounded bg-white'>
|
||||
<div className='max-h-[200px] flex flex-col border border-ut-offwhite/50 rounded bg-white'>
|
||||
<Disclosure defaultOpen={defaultOpen}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -54,17 +54,17 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
||||
|
||||
<Transition
|
||||
as='div'
|
||||
className='overflow-hidden'
|
||||
className='flex flex-1 flex-col overflow-y-hidden'
|
||||
enter='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
||||
enterFrom='max-h-0 opacity-0 p-0.5'
|
||||
enterTo='max-h-[440px] opacity-100 p-0'
|
||||
enterTo='max-h-[200px] opacity-100 p-0'
|
||||
leave='transition-[max-height,opacity,padding] duration-300 ease-in-out-expo'
|
||||
leaveFrom='max-h-[440px] opacity-100 p-0'
|
||||
leaveFrom='max-h-[200px] opacity-100 p-0'
|
||||
leaveTo='max-h-0 opacity-0 p-0.5'
|
||||
>
|
||||
<div className='px-3.5 pb-2.5 pt-2'>
|
||||
<DisclosurePanel>{children}</DisclosurePanel>
|
||||
</div>
|
||||
<DisclosurePanel className='mx-1.75 mb-2.5 mt-2 flex flex-1 flex-col overflow-y-auto'>
|
||||
<div className='mx-1.75'>{children}</div>
|
||||
</DisclosurePanel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
function getCourseSections() {
|
||||
const table = document.querySelector('table');
|
||||
if (!table) return [];
|
||||
|
||||
const rows = Array.from(table.querySelectorAll('tr'));
|
||||
const sections: { header: HTMLTableRowElement; children: HTMLTableRowElement[] }[] = [];
|
||||
let currentSection: { header: HTMLTableRowElement; children: HTMLTableRowElement[] } | null = null;
|
||||
|
||||
for (const row of rows) {
|
||||
const headerCell = row.querySelector('td.course_header');
|
||||
if (headerCell) {
|
||||
if (currentSection) sections.push(currentSection);
|
||||
currentSection = { header: row, children: [] };
|
||||
} else if (currentSection) {
|
||||
currentSection.children.push(row);
|
||||
}
|
||||
}
|
||||
if (currentSection) sections.push(currentSection);
|
||||
return sections;
|
||||
}
|
||||
|
||||
const CollapsibleSection: React.FC<{
|
||||
header: HTMLTableRowElement;
|
||||
childrenRows: HTMLTableRowElement[];
|
||||
}> = ({ header, childrenRows }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Hide children rows initially
|
||||
childrenRows.forEach(row => (row.style.display = open ? '' : 'none'));
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
childrenRows.forEach(row => (row.style.display = ''));
|
||||
};
|
||||
}, [open, childrenRows]);
|
||||
|
||||
// Inject a button into the header cell
|
||||
useEffect(() => {
|
||||
const cell = header.querySelector('td.course_header');
|
||||
if (!cell) return;
|
||||
let button = cell.querySelector('.utrp-collapse-btn') as HTMLButtonElement | null;
|
||||
if (!button) {
|
||||
button = document.createElement('button');
|
||||
button.className = 'utrp-collapse-btn';
|
||||
button.style.marginRight = '8px';
|
||||
cell.prepend(button);
|
||||
}
|
||||
button.textContent = open ? '▼' : '►';
|
||||
button.onclick = () => setOpen(o => !o);
|
||||
// Clean up
|
||||
return () => {
|
||||
button?.remove();
|
||||
};
|
||||
}, [header, open]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default
|
||||
@@ -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
|
||||
<Text variant='small' className='text-ut-black'>
|
||||
Grade Distribution for{' '}
|
||||
<Text variant='small' className='font-extrabold!' as='strong'>
|
||||
{course.department} {course.number}
|
||||
{course.department} {course.getNumberWithoutTerm()}
|
||||
</Text>
|
||||
</Text>
|
||||
<select
|
||||
@@ -267,7 +267,8 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
||||
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3 text-center'>
|
||||
<Text variant='small' className='text-theme-red'>
|
||||
We couldn't find {semester !== 'Aggregate' && ` ${semester}`} grades for this
|
||||
instructor, so here are the grades for all {course.department} {course.number} sections.
|
||||
instructor, so here are the grades for all {course.department}{' '}
|
||||
{course.getNumberWithoutTerm()} sections.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
34
src/views/lib/CourseCatalogScraper.test.ts
Normal file
34
src/views/lib/CourseCatalogScraper.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { CourseCatalogScraper } from './CourseCatalogScraper';
|
||||
|
||||
describe('CourseCatalogScraper::separateCourseName', () => {
|
||||
it('should separate a simple course', () => {
|
||||
// UT Formats strings weird... lots of meaningless spaces
|
||||
const input = 'C S 314H DATA STRUCTURES: HONORS ';
|
||||
const expected = ['DATA STRUCTURES: HONORS', 'C S', '314H'];
|
||||
const result = CourseCatalogScraper.separateCourseName(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('separate summer courses ', () => {
|
||||
// UT Formats strings weird... lots of meaningless spaces
|
||||
const inputs = [
|
||||
'P R f378 PUBLIC RELATNS TECHNIQUES-IRL (First term) ',
|
||||
'CRP s396 INDEPENDENT RESEARCH IN CRP (Second term) ',
|
||||
'B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term) ',
|
||||
'J w379 JOURNALISM INDEPENDENT STUDY (Whole term) ',
|
||||
];
|
||||
|
||||
const expected = [
|
||||
['PUBLIC RELATNS TECHNIQUES-IRL (First term)', 'P R', 'f378'],
|
||||
['INDEPENDENT RESEARCH IN CRP (Second term)', 'CRP', 's396'],
|
||||
['1-MANAGERIAL MICROECON-I-DAL (Nine week term)', 'B A', 'n284S'],
|
||||
['JOURNALISM INDEPENDENT STUDY (Whole term)', 'J', 'w379'],
|
||||
];
|
||||
const results = inputs.map(input => CourseCatalogScraper.separateCourseName(input));
|
||||
|
||||
expect(results).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -75,7 +75,7 @@ export class CourseCatalogScraper {
|
||||
|
||||
fullName = fullName.replace(/\s\s+/g, ' ').trim();
|
||||
|
||||
const [courseName, department, number] = this.separateCourseName(fullName);
|
||||
const [courseName, department, number] = CourseCatalogScraper.separateCourseName(fullName);
|
||||
const [status, isReserved] = this.getStatus(row);
|
||||
|
||||
const newCourse = new Course({
|
||||
@@ -113,16 +113,31 @@ export class CourseCatalogScraper {
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* separateCourseName("CS 314H - Honors Discrete Structures") => ["Honors Discrete Structures", "CS", "314H"]
|
||||
* separateCourseName("C S 314H DATA STRUCTURES: HONORS") => ["DATA STRUCTURES: HONORS", "C S", "314H"]
|
||||
* ```
|
||||
* @param courseFullName - the full name of the course (e.g. "CS 314H - Honors Discrete Structures")
|
||||
* @param courseFullName - the full name of the course (e.g. "C S 314H DATA STRUCTURES: HONORS")
|
||||
* @returns an array of the course name, department, and number
|
||||
*/
|
||||
separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
||||
let courseNumberIndex = courseFullName.search(/\d/);
|
||||
let department = courseFullName.substring(0, courseNumberIndex).trim();
|
||||
let number = courseFullName.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
||||
let courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
||||
static separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
||||
// C S 314H DATA STRUCTURES: HONORS
|
||||
// ^ Here for normal courses
|
||||
// B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term)
|
||||
// ^ Also works for summer courses ([f]irst term, [s]econd term, [n]ine week term, [w]hole term)
|
||||
const courseNumberIndex = courseFullName.search(/\w?\d/);
|
||||
|
||||
if (courseNumberIndex === -1) {
|
||||
throw new Error("Course name doesn't have a course number");
|
||||
}
|
||||
|
||||
// Everything before the course number
|
||||
const department = courseFullName.substring(0, courseNumberIndex).trim();
|
||||
|
||||
const number = courseFullName
|
||||
.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex))
|
||||
.trim();
|
||||
|
||||
// Everything after the course number
|
||||
const courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
||||
|
||||
return [courseName, department, number];
|
||||
}
|
||||
|
||||
@@ -109,16 +109,22 @@ function generateQuery(
|
||||
includeInstructor: boolean
|
||||
): [string, GradeDistributionParams] {
|
||||
const query = `
|
||||
select * from grade_distributions
|
||||
where Department_Code = :department_code
|
||||
and Course_Number = :course_number
|
||||
${includeInstructor ? `and Instructor_Last = :instructor_last collate nocase` : ''}
|
||||
${semester ? `and Semester = :semester` : ''}
|
||||
SELECT * FROM grade_distributions
|
||||
WHERE Department_Code = :department_code
|
||||
AND Course_Number COLLATE NOCASE IN (
|
||||
:course_number,
|
||||
concat('F', :course_number), -- Check summer courses with prefix, too
|
||||
concat('S', :course_number),
|
||||
concat('N', :course_number),
|
||||
concat('W', :course_number)
|
||||
)
|
||||
${includeInstructor ? `AND Instructor_Last = :instructor_last COLLATE NOCASE` : ''}
|
||||
${semester ? `AND Semester = :semester` : ''}
|
||||
`;
|
||||
|
||||
const params: GradeDistributionParams = {
|
||||
':department_code': course.department,
|
||||
':course_number': course.number,
|
||||
':course_number': course.getNumberWithoutTerm(),
|
||||
};
|
||||
|
||||
if (includeInstructor) {
|
||||
|
||||
Reference in New Issue
Block a user