Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

feat: ✨ add previews for courses/instructors #119

Merged
merged 8 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions apps/api/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Course as PrismaCourse } from "@libs/db";
import { Course, CourseLevel, GECategory, PrerequisiteTree } from "@peterportal-api/types";
import {
Course,
CourseLevel,
CoursePreview,
GECategory,
InstructorPreview,
PrerequisiteTree,
} from "@peterportal-api/types";
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved

const days = ["Su", "M", "Tu", "W", "Th", "F", "Sa"];

Expand Down Expand Up @@ -64,11 +71,14 @@ export function normalizeCourse(course: PrismaCourse): Course {
return {
...course,
courseLevel,
instructorHistory: course.instructorHistory as unknown as string[],
instructorHistory: course.instructorHistory,
instructors: course.instructors as unknown as InstructorPreview[],
prerequisiteTree: course.prerequisiteTree as unknown as PrerequisiteTree,
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
prerequisiteList: course.prerequisiteList as unknown as string[],
prerequisiteFor: course.prerequisiteFor as unknown as string[],
prerequisiteList: course.prerequisiteList,
prerequisiteFor: course.prerequisiteFor,
prerequisites: course.prerequisites as unknown as CoursePreview[],
dependencies: course.dependencies as unknown as CoursePreview[],
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
geList,
terms: course.terms as unknown as string[],
terms: course.terms,
};
}
18 changes: 18 additions & 0 deletions apps/api/src/routes/v1/graphql/schema/courses.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
"An object that contains a subset of a course's metadata, for preview purposes."
type CoursePreview {
"The course ID."
id: String!
"The department code that the course belongs to."
department: String!
"The course number of the course."
courseNumber: String!
"The title of the course."
title: String!
}

"An object that represents a course."
type Course {
"The course ID."
Expand Down Expand Up @@ -52,6 +64,12 @@ type Course {
geText: String!
"The list of terms in which this course was offered."
terms: [String!]!
"The previews for the instructors that have taught this course in the past."
instructors: [InstructorPreview!]!
"The previews for the courses that are required to take this course."
prerequisites: [CoursePreview!]!
"The previews for the courses that require this course."
dependencies: [CoursePreview!]!
}

extend type Query {
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/routes/v1/graphql/schema/instructors.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
"An object that contains a subset of an instructor's metadata, for preview purposes."
type InstructorPreview {
"The instructor's UCINetID."
ucinetid: String!
"The full name of the instructor."
name: String!
"The shortened name (or WebSoc name; e.g. ``SHINDLER, M.``) of the instructor."
shortenedName: String!
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
}

"An object representing an instructor."
type Instructor {
"The instructor's UCINetID."
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/routes/v1/rest/courses/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ export function constructPrismaQuery(parsedQuery: Query): Prisma.CourseWhereInpu
if (parsedQuery.taughtByInstructors)
AND.push({
OR: parsedQuery.taughtByInstructors.map((instructor) => ({
instructorHistory: { array_contains: [instructor.toLowerCase()] },
instructorHistory: { has: instructor.toLowerCase() },
})),
});

if (parsedQuery.geCategory && parsedQuery.geCategory !== "ANY")
AND.push({ geList: { array_contains: [parsedQuery.geCategory] } });
AND.push({ geList: { has: parsedQuery.geCategory } });

if (parsedQuery.taughtInTerms)
AND.push({
OR: parsedQuery.taughtInTerms.map((term) => ({ terms: { array_contains: [term] } })),
OR: parsedQuery.taughtInTerms.map((term) => ({ terms: { has: term } })),
});

return { AND: AND.length ? AND : undefined };
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/routes/v1/rest/instructors/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export function constructPrismaQuery(parsedQuery: Query): Prisma.InstructorWhere

if (parsedQuery.schoolsContains)
AND.push({
OR: parsedQuery.schoolsContains.map((school) => ({ schools: { array_contains: [school] } })),
OR: parsedQuery.schoolsContains.map((school) => ({ schools: { has: school } })),
});

if (parsedQuery.relatedDepartmentsContains)
AND.push({
OR: parsedQuery.relatedDepartmentsContains.map((dept) => ({
relatedDepartments: { array_contains: [dept.toUpperCase()] },
relatedDepartments: { has: dept.toUpperCase() },
})),
});

Expand Down
20 changes: 12 additions & 8 deletions libs/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,37 @@ model Course {
maxUnits Float
description String @db.Text
departmentName String
instructorHistory Json
instructorHistory String[]
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
instructors Json @default("[]")
prerequisiteTree Json
prerequisiteList Json
prerequisiteList String[]
prerequisiteText String @db.Text
prerequisiteFor Json
prerequisiteFor String[]
prerequisites Json @default("[]")
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
dependencies Json @default("[]")
repeatability String
gradingOption String
concurrent String
sameAs String
restriction String @db.Text
overlap String
corequisites String
geList Json
geList String[]
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
geText String
terms Json
terms String[]
}

model Instructor {
ucinetid String @id
ucinetid String @id
name String
shortenedName String
title String
email String
department String
schools Json
relatedDepartments Json
schools String[]
relatedDepartments String[]
courseHistory Json
courses Json @default("[]")
}

model GradesInstructor {
Expand Down
22 changes: 20 additions & 2 deletions packages/types/types/courses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CourseLevel, GECategory } from "./constants";
import { InstructorPreview } from "./instructor";

/**
* An object representing a prerequisite.
Expand Down Expand Up @@ -44,6 +45,11 @@ export type PrerequisiteTree = {
NOT?: Array<Prerequisite | PrerequisiteTree>;
};

/**
* An object that contains a subset of a course's metadata, for preview purposes.
*/
export type CoursePreview = Pick<Course, "id" | "department" | "courseNumber" | "title">;

/**
* An object that represents a course.
* The type of the payload returned on a successful response from querying
Expand Down Expand Up @@ -103,15 +109,15 @@ export type Course = {
*/
prerequisiteTree: PrerequisiteTree;
/**
* The list of prerequisites for the course.
* The list of prerequisites' IDs for the course.
*/
prerequisiteList: string[];
/**
* The catalogue's prerequisite text for the course.
*/
prerequisiteText: string;
/**
* The courses for which this course is a prerequisite.
* The IDs of the courses for which this course is a prerequisite.
*/
prerequisiteFor: string[];
/**
Expand Down Expand Up @@ -154,6 +160,18 @@ export type Course = {
* The list of terms in which this course was offered.
*/
terms: string[];
/**
* The previews for the instructors that have taught this course in the past.
*/
instructors: InstructorPreview[];
/**
* The previews for the courses that are required to take this course.
*/
prerequisites: CoursePreview[];
/**
* The previews for the courses that require this course.
*/
dependencies: CoursePreview[];
};

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/types/types/instructor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CoursePreview } from "./courses";

/**
* An object representing an instructor.
* The type of the payload returned on a successful response from querying
Expand Down Expand Up @@ -42,10 +44,19 @@ export type Instructor = {
* the instructor taught the corresponding course.
*/
courseHistory: Record<string, string[]>;
/**
* The previews for the course(s) this instructor has taught in the past.
*/
courses: CoursePreview[];
};

/**
* The type of the payload returned on a successful response from querying
* ``/v1/rest/instructors/all``.
*/
export type Instructors = Instructor[];

/**
* An object that contains a subset of an instructor's metadata, for preview purposes.
*/
export type InstructorPreview = Pick<Instructor, "ucinetid" | "name" | "shortenedName">;
61 changes: 61 additions & 0 deletions tools/registrar-scraper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,67 @@ async function main() {
data: Object.values(instructorInfo),
}),
]);
const courses = Object.fromEntries((await prisma.course.findMany()).map((x) => [x.id, x]));
const instructors = Object.fromEntries(
(await prisma.instructor.findMany()).map((x) => [x.ucinetid, x]),
);
const newCourses = [];
for (const course of Object.values(courses)) {
newCourses.push({
...course,
prerequisiteTree: course.prerequisiteTree as object,
instructors: course.instructorHistory
.map((x) => instructors[x])
.filter((x) => x)
.map(({ ucinetid, name, shortenedName }) => ({ ucinetid, name, shortenedName })),
prerequisites: course.prerequisiteList
.map((x) => courses[x.replace(/ /g, "")])
.filter((x) => x)
.map(({ id, title, department, courseNumber }) => ({
id,
title,
department,
courseNumber,
})),
dependencies: course.prerequisiteFor
.map((x) => courses[x.replace(/ /g, "")])
.filter((x) => x)
.map(({ id, title, department, courseNumber }) => ({
id,
title,
department,
courseNumber,
})),
});
}
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
const newInstructors = [];
for (const instructor of Object.values(instructors)) {
newInstructors.push({
...instructor,
courseHistory: instructor.courseHistory as object,
courses: Object.keys(instructor.courseHistory!)
.map((x) => courses[x.replace(/ /g, "")])
.filter((x) => x)
.map(({ id, title, department, courseNumber }) => ({
id,
title,
department,
courseNumber,
})),
});
}
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
await prisma.$transaction([
prisma.course.deleteMany({ where: { id: { in: Object.keys(courses) } } }),
prisma.instructor.deleteMany({ where: { ucinetid: { in: Object.keys(instructors) } } }),
prisma.course.createMany({
data: newCourses,
skipDuplicates: true,
}),
prisma.instructor.createMany({
data: newInstructors,
skipDuplicates: true,
}),
]);
}

main().then();
8 changes: 4 additions & 4 deletions tools/registrar-scraper/src/instructor-scraper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type InstructorsData = {
};

type InstructorsInfo = {
[ucinetid: string]: Instructor;
[ucinetid: string]: Omit<Instructor, "courses">;
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
};

type InstructorsLog = {
Expand Down Expand Up @@ -139,7 +139,7 @@ export async function getInstructors(
}
});
});
const instructorPromises: Promise<[string, Instructor]>[] = [];
const instructorPromises: Promise<[string, Omit<Instructor, "courses">]>[] = [];
ecxyzzy marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(instructorsDict).forEach((name) => {
const schools = instructorsDict[name].schools;
const related_departments = Array.from(instructorsDict[name].courses);
Expand Down Expand Up @@ -214,9 +214,9 @@ async function getInstructor(
relatedDepartments: string[],
attempts: number,
year_threshold: number,
): Promise<[string, Instructor]> {
): Promise<[string, Omit<Instructor, "courses">]> {
logger.info(`Scraping data for ${instructorName}`);
const instructorObject: Instructor = {
const instructorObject: Omit<Instructor, "courses"> = {
name: instructorName,
ucinetid: "",
title: "",
Expand Down