Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ add filtering by restriction code to WebSoc endpoint #130

Merged
merged 14 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
23 changes: 23 additions & 0 deletions apps/api/src/routes/v1/graphql/schema/enum.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ enum SectionType {
Tap
Tut
}

"The set of valid restriction codes."
enum RestrictionCode {
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
R
S
X
}

"The set of valid options for filtering full courses."
enum FullCourses {
ANY
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1/graphql/schema/websoc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ extend type Query {
maxCapacity: String
fullCourses: FullCourses
cancelledCourses: CancelledCourses
excludeRestrictionCodes: [RestrictionCode!]
): WebsocAPIResponse!
"Get data on all available departments."
depts: [Department!]!
Expand Down
41 changes: 39 additions & 2 deletions apps/api/src/routes/v1/rest/websoc/+endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PrismaClient } from "@libs/db";
import { createHandler } from "@libs/lambda";
import type { WebsocAPIResponse } from "@libs/uc-irvine-lib/websoc";
import type { WebsocAPIResponse, WebsocSchool } from "@libs/uc-irvine-lib/websoc";
import { notNull } from "@libs/utils";
import { combineAndNormalizeResponses, sortResponse } from "@libs/websoc-utils";
import type { z } from "zod";
import { ZodError } from "zod";

import { APILambdaClient } from "./APILambdaClient";
Expand Down Expand Up @@ -111,7 +112,9 @@ export const GET = createHandler(async (event, context, res) => {
queries: normalizeQuery(parsedQuery),
});

return res.createOKResult(websocResults, headers, requestId);
const filteredWebsocResults = filterResults(parsedQuery, websocResults);

return res.createOKResult(filteredWebsocResults, headers, requestId);
} catch (error) {
if (error instanceof ZodError) {
const messages = error.issues.map((issue) => issue.message);
Expand All @@ -120,3 +123,37 @@ export const GET = createHandler(async (event, context, res) => {
return res.createErrorResult(400, error, requestId);
}
}, onWarm);

function filterResults(query: z.infer<typeof QuerySchema>, websocResults: WebsocAPIResponse) {
if (!query.excludeRestrictionCodes) {
return websocResults;
}

const excludeRestrictions = query.excludeRestrictionCodes ?? [];

if (excludeRestrictions.length > 0) {
websocResults.schools = websocResults.schools
.map((school) => {
school.departments = school.departments
.map((department) => {
department.courses = department.courses
.map((course) => {
course.sections = course.sections.filter(
(section) =>
!section.restrictions
.split(/ and | or /)
.some((code: string) => excludeRestrictions.includes(code)),
);
return course;
})
.filter((course) => course.sections.length > 0);
return { ...department, courses: department.courses };
andrew-wang0 marked this conversation as resolved.
Show resolved Hide resolved
})
.filter((department) => department.courses.length > 0);
return { ...school, departments: school.departments };
})
.filter((school) => school.departments.length > 0) satisfies WebsocSchool[];
}

return websocResults;
}
11 changes: 11 additions & 0 deletions apps/api/src/routes/v1/rest/websoc/lib.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { $Enums } from "@libs/db";
import type { Prisma } from "@libs/db";
import type { WebsocAPIOptions } from "@libs/uc-irvine-lib/websoc";
import { notNull } from "@libs/utils";
Expand Down Expand Up @@ -200,6 +201,16 @@ export function constructPrismaQuery(parsedQuery: Query): Prisma.WebsocSectionWh
);
}

if (parsedQuery.excludeRestrictionCodes) {
AND.push({
NOT: {
restrictionCodes: {
hasSome: parsedQuery.excludeRestrictionCodes as $Enums.RestrictionCode[],
},
},
});
}

return {
AND,

Expand Down
19 changes: 18 additions & 1 deletion apps/api/src/routes/v1/rest/websoc/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { $Enums } from "@libs/db";
import {
anyArray,
cancelledCoursesOptions,
Expand Down Expand Up @@ -51,6 +52,12 @@ export const QuerySchema = z
units: z.string().array().or(z.string()).optional().transform(flattenStringsAndSplit),
startTime: z.optional(z.literal("").or(z.string().regex(/([1-9]|1[0-2]):[0-5][0-9][ap]m/))),
endTime: z.optional(z.literal("").or(z.string().regex(/([1-9]|1[0-2]):[0-5][0-9][ap]m/))),
excludeRestrictionCodes: z
.string()
.array()
.or(z.string())
.optional()
.transform(flattenStringsAndSplit),
})
.refine((x) => x.cache || !x.cacheOnly, {
message: "cacheOnly cannot be true if cache is false",
Expand All @@ -67,7 +74,17 @@ export const QuerySchema = z
)
.refine((x) => x.cacheOnly || x.building || !x.room, {
message: 'If "building" is provided, "room" must also be provided',
});
})
.refine(
(x) =>
!x.excludeRestrictionCodes ||
x.excludeRestrictionCodes.every((code) =>
Object.values($Enums.RestrictionCode).includes(code as $Enums.RestrictionCode),
),
{
message: `Restriction codes must be in [${Object.values($Enums.RestrictionCode).join(", ")}]`,
},
);

/**
* Type of the parsed query: useful for passing the query as input to other functions.
Expand Down
60 changes: 41 additions & 19 deletions libs/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ enum WebsocSectionType {
Tut
}

enum RestrictionCode {
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
R
S
X
}

// Models

model CalendarTerm {
Expand Down Expand Up @@ -224,25 +245,26 @@ model WebsocSectionMeeting {
}

model WebsocSection {
year String
quarter Quarter
sectionCode Int
timestamp DateTime
geCategories Json
department String
courseNumber String
courseNumeric Int
instructors WebsocSectionInstructor[]
courseTitle String
sectionType WebsocSectionType
units String
meetings WebsocSectionMeeting[]
maxCapacity Int
sectionFull Boolean
waitlistFull Boolean
overEnrolled Boolean
cancelled Boolean
data Json
year String
quarter Quarter
sectionCode Int
timestamp DateTime
geCategories Json
department String
courseNumber String
courseNumeric Int
instructors WebsocSectionInstructor[]
courseTitle String
sectionType WebsocSectionType
units String
meetings WebsocSectionMeeting[]
maxCapacity Int
sectionFull Boolean
waitlistFull Boolean
overEnrolled Boolean
cancelled Boolean
restrictionCodes RestrictionCode[]
data Json

@@id([year, quarter, sectionCode, timestamp])
@@unique([year, quarter, sectionCode, timestamp], name: "idx")
Expand Down
5 changes: 5 additions & 0 deletions services/websoc-scraper-v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { $Enums } from "@libs/db";
import { PrismaClient } from "@libs/db";
import { getTermDateData } from "@libs/uc-irvine-lib/registrar";
import type {
Expand Down Expand Up @@ -116,6 +117,7 @@ type ProcessedSection = {
waitlistFull: boolean;
overEnrolled: boolean;
cancelled: boolean;
restrictionCodes: $Enums.RestrictionCode[];
data: object;
};
};
Expand Down Expand Up @@ -391,6 +393,9 @@ async function scrape(name: string, term: Term) {
parseInt(section.numCurrentlyEnrolled.totalEnrolled, 10) >
parseInt(section.maxCapacity, 10),
cancelled: section.sectionComment.includes("*** CANCELLED ***"),
restrictionCodes: section.restrictions
? (section.restrictions.split(/ and | or /) as $Enums.RestrictionCode[])
: [],
data: isolateSection({ school, department, course, section }),
},
};
Expand Down
Loading