feat: UTRP v2 migration (#292)

* feat: wip add course by url

* chore: update imports

* feat: add useCourseFromUrl hook

* chore: extract logic into async function

* feat: add checkLoginStatus.ts

* feat: add useCourseMigration hook

* feat: working course migration

* fix: active schedule bug

* feat: refactor logic and add to onUpdate

* feat: update ui style

* feat: add changelog functionality to settings

* chore: update packages

* feat: migration + sentry stuffs

* feat: improve migration flow

* docs: add sentry jsdocs

* chore: fix lint and format

* chore: cleanup + fix race condition

---------

Co-authored-by: Samuel Gunter <sgunter@utexas.edu>
Co-authored-by: Razboy20 <razboy20@gmail.com>
This commit is contained in:
doprz
2024-10-14 21:30:37 -05:00
committed by GitHub
parent e774f316e3
commit d22237d561
23 changed files with 1980 additions and 1865 deletions

View File

@@ -38,9 +38,13 @@ type DetailsSelectorType = (typeof DetailsSelector)[keyof typeof DetailsSelector
*/
export class CourseCatalogScraper {
support: SiteSupportType;
doc: Document;
url: string;
constructor(support: SiteSupportType) {
constructor(support: SiteSupportType, doc: Document = document, url: string = window.location.href) {
this.support = support;
this.doc = doc;
this.url = url;
}
/**
@@ -91,7 +95,7 @@ export class CourseCatalogScraper {
uniqueId: this.getUniqueId(row),
instructionMode: this.getInstructionMode(row),
instructors: this.getInstructors(row) as Instructor[],
description: this.getDescription(document),
description: this.getDescription(this.doc),
semester: this.getSemester(),
scrapedAt: Date.now(),
colors: getCourseColors('emerald', 500),
@@ -165,7 +169,7 @@ export class CourseCatalogScraper {
*/
getURL(row: HTMLTableRowElement): string {
const div = row.querySelector<HTMLAnchorElement>(`${TableDataSelector.UNIQUE_ID} a`);
return div?.href || window.location.href;
return div?.href || this.url;
}
/**
@@ -221,11 +225,11 @@ export class CourseCatalogScraper {
/**
* Scrapes the description of the course from the course details page and separates it into an array of cleaned up lines
* @param document the document of the course details page to scrape
* @param doc the document of the course details page to scrape
* @returns an array of lines of the course description
*/
getDescription(document: Document): string[] {
const lines = document.querySelectorAll(DetailsSelector.COURSE_DESCRIPTION);
getDescription(doc: Document): string[] {
const lines = doc.querySelectorAll(DetailsSelector.COURSE_DESCRIPTION);
return Array.from(lines)
.map(line => line.textContent || '')
.map(line => line.replace(/\s\s+/g, ' ').trim())
@@ -233,7 +237,7 @@ export class CourseCatalogScraper {
}
getSemester(): Semester {
const { pathname } = new URL(window.location.href);
const { pathname } = new URL(this.url);
const code = pathname.split('/')[4];
if (!code) {
@@ -276,7 +280,7 @@ export class CourseCatalogScraper {
*/
getFullName(row?: HTMLTableRowElement): string {
if (!row) {
return document.querySelector(DetailsSelector.COURSE_NAME)?.textContent || '';
return this.doc.querySelector(DetailsSelector.COURSE_NAME)?.textContent || '';
}
const div = row.querySelector(TableDataSelector.COURSE_HEADER);
return div?.textContent || '';

View File

@@ -0,0 +1,51 @@
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import getCourseTableRows from '@views/lib/getCourseTableRows';
import { SiteSupport } from '@views/lib/getSiteSupport';
/**
* Migrates courses from UTRP v1 to a new schedule.
*
* @param activeSchedule - The active schedule to migrate the courses to.
* @param links - An array of UTRP v1 course URLs.
* @returns A promise that resolves when the migration is complete.
*
* This hook performs the following steps:
* 1. Fetches the course details from the links.
* 2. Scrapes the course information from the fetched HTML.
* 3. Checks if the course was found and adds it to the active schedule if it doesn't already exist.
*
* Notes:
* - Chrome warns in the console that in the future, cookies will not work when we do a network request like how we are doing it now, so might need to open a new tab instead.
*/
export const courseMigration = async (links: string[]) => {
const migratedCourses = [];
// Loop over the links
for (const link of links) {
// eslint-disable-next-line no-await-in-loop
const response = await fetch(link);
// eslint-disable-next-line no-await-in-loop
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
// Scrape the course
const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS, doc, link);
const tableRows = getCourseTableRows(doc);
const courses = scraper.scrape(tableRows, false);
// Check if the course was found
if (courses.length === 1) {
const description = scraper.getDescription(doc);
const row = courses[0]!;
const course = row.course!;
course.description = description;
// Add the course to the migrated courses
migratedCourses.push(course);
} else {
console.warn('Invalid course link:', link);
}
}
return migratedCourses;
};