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:
@@ -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 || '';
|
||||
|
||||
51
src/views/lib/courseMigration.ts
Normal file
51
src/views/lib/courseMigration.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user