Skip to content

Commit

Permalink
feat: ✨ add filtering by restriction code to WebSoc endpoint (#130)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Wu <[email protected]>
Co-authored-by: Aponia <[email protected]>
  • Loading branch information
3 people authored May 15, 2024
1 parent 4bbd53d commit 74889e2
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 22 deletions.
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
34 changes: 32 additions & 2 deletions apps/api/src/routes/v1/rest/websoc/+endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { QuerySchema } from "./schema";

const prisma = new PrismaClient();

// let connected = false
const lambdaClient = await APILambdaClient.new();

async function onWarm() {
Expand Down Expand Up @@ -111,7 +110,13 @@ export const GET = createHandler(async (event, context, res) => {
queries: normalizeQuery(parsedQuery),
});

return res.createOKResult(websocResults, headers, requestId);
if (!parsedQuery.excludeRestrictionCodes?.length) {
return res.createOKResult(websocResults, headers, requestId);
}

const filteredWebsocResults = filterResults(websocResults, parsedQuery.excludeRestrictionCodes);

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

function filterResults(results: WebsocAPIResponse, restrictionCodes: string[]): WebsocAPIResponse {
results.schools = results.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) => restrictionCodes.includes(code)),
);
return course;
})
.filter((course) => course.sections.length > 0);
return department;
})
.filter((department) => department.courses.length > 0);
return school;
})
.filter((school) => school.departments.length > 0);
return results;
}
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
23 changes: 22 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,21 @@ export const QuerySchema = z
)
.refine((x) => x.cacheOnly || x.building || !x.room, {
message: 'If "building" is provided, "room" must also be provided',
});
})
.refine(
(x) => {
// If not excluding restriction codes, then no more validation is needed.
if (x.excludeRestrictionCodes == null) return true;

// Ensure that all provided restriction codes are valid.
return 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

0 comments on commit 74889e2

Please sign in to comment.