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

feat: ✨ add filtering by GE and additional aggregation operations #88

Merged
merged 23 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2743fdf
feat: :sparkles: add aggregateGrouped REST operation
ecxyzzy Aug 27, 2023
de61485
feat: :sparkles: add GraphQL support
ecxyzzy Aug 27, 2023
bb0100d
docs: :books: add docs for new operation
ecxyzzy Aug 31, 2023
6fb8ba0
chore: 🔧 merge main into aggregate-grouped
ecxyzzy Aug 31, 2023
f1e5ac5
chore: 🔧 suppress wfs index errors
ecxyzzy Aug 31, 2023
d9d2bc1
fix(graphql): 🐛 use correct field name
ecxyzzy Aug 31, 2023
624fa56
chore: 🔧 merge main into aggregate-grouped
ecxyzzy Sep 4, 2023
87868e9
feat: ✨ add geCategories to GradesSection
ecxyzzy Sep 4, 2023
3b63c90
chore: 🔧 merge main into aggregate-grouped
ecxyzzy Sep 5, 2023
fa63f38
feat(grades): ✨ add script for populating GE data
ecxyzzy Sep 5, 2023
b5c3f4f
feat: ✨ add GE param to schema
ecxyzzy Sep 5, 2023
24ae0d9
fix: 🐛 add geCategories to aggregate endpoint
ecxyzzy Sep 5, 2023
4e9eabd
docs: 📚️ update params
ecxyzzy Sep 5, 2023
98e0034
fix: 🐛 cast GE category arrays
ecxyzzy Sep 5, 2023
0020a7d
fix: 🐛 add params to graphql queries
ecxyzzy Sep 7, 2023
9770c6d
fix: 🐛 add GE transform to graphql queries
ecxyzzy Sep 7, 2023
028721c
perf: ⚡️ test whether local filtering improves response time
ecxyzzy Sep 7, 2023
3a770dd
docs: 📚️ update grades updater docs [skip ci]
ecxyzzy Sep 7, 2023
3fec2df
perf: ⚡️ use one column per GE category
ecxyzzy Sep 7, 2023
f2b0d08
feat!: 💥 ✨ add aggregateByCourse endpoint
ecxyzzy Sep 7, 2023
6a5b147
docs: 📚️ update type names
ecxyzzy Sep 7, 2023
484a314
chore: 🔧 merge main into aggregate-grouped
ecxyzzy Sep 14, 2023
e2b4d87
fix: 🐛 use correct type for aggregateByCourse
ecxyzzy Sep 18, 2023
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
38 changes: 38 additions & 0 deletions apps/api/v1/graphql/src/graphql/grades.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,33 @@ type GradesOptions {
"The list of instructors that matched the given filters."
instructors: [String!]!
}
"An object that represents aggregate grades statistics for a course taught by an instructor."
type AggregateGroupedGrade {
"The department code."
department: String!
"The course number the section belongs to."
courseNumber: String!
"The shortened name of the instructor who taught the section."
instructor: String!
"How many students attained an A+/A/A-."
gradeACount: Int!
"How many students attained a B+/B/B-."
gradeBCount: Int!
"How many students attained a C+/C/C-."
gradeCCount: Int!
"How many students attained a D+/D/D-."
gradeDCount: Int!
"How many students attained an F."
gradeFCount: Int!
"How many students attained a P."
gradePCount: Int!
"How many students attained an NP."
gradeNPCount: Int!
"How many students attained a W."
gradeWCount: Int!
"The average GPA of all assigned grades in the course."
averageGPA: Float!
}

extend type Query {
"Get the raw grade info for the given parameters."
Expand Down Expand Up @@ -126,4 +153,15 @@ extend type Query {
division: Division
excludePNP: Boolean
): GradesOptions!
"Get the aggregate grade info, grouped by course and instructor, for the given parameters."
aggregateGroupedGrades(
year: String
quarter: Quarter
instructor: String
department: String
courseNumber: String
sectionCode: String
division: Division
excludePNP: Boolean
): [AggregateGroupedGrade!]!
}
1 change: 1 addition & 0 deletions apps/api/v1/graphql/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const resolvers: ApolloServerOptions<BaseContext>["resolvers"] = {
rawGrades: proxyRestApi("v1/rest/grades/raw"),
aggregateGrades: proxyRestApi("v1/rest/grades/aggregate"),
gradesOptions: proxyRestApi("v1/rest/grades/options"),
aggregateGroupedGrades: proxyRestApi("v1/rest/grades/aggregateGrouped"),
instructor: proxyRestApi("v1/rest/instructors", { pathArg: "courseId" }),
instructors: proxyRestApi("v1/rest/instructors"),
allInstructors: proxyRestApi("v1/rest/instructors/all"),
Expand Down
23 changes: 20 additions & 3 deletions apps/api/v1/rest/grades/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PrismaClient } from "@libs/db";
import { createErrorResult, createOKResult, logger } from "ant-stack";
import type { InternalHandler } from "ant-stack";
import type { GradesOptions, GradesRaw } from "peterportal-api-next-types";
import type { AggregateGroupedGrades, GradesOptions, RawGrades } from "peterportal-api-next-types";
import { ZodError } from "zod";

import { aggregateGrades, constructPrismaQuery, lexOrd } from "./lib";
import { aggregateGrades, aggregateGroupedGrades, constructPrismaQuery, lexOrd } from "./lib";
import { QuerySchema } from "./schema";

let prisma: PrismaClient;
Expand Down Expand Up @@ -39,7 +39,7 @@ export const GET: InternalHandler = async (request) => {
}));
switch (params.id) {
case "raw":
return createOKResult<GradesRaw>(res, headers, requestId);
return createOKResult<RawGrades>(res, headers, requestId);
case "aggregate":
return createOKResult(aggregateGrades(res), headers, requestId);
}
Expand Down Expand Up @@ -97,6 +97,23 @@ export const GET: InternalHandler = async (request) => {
requestId,
);
}
case "aggregateGrouped": {
return createOKResult<AggregateGroupedGrades>(
aggregateGroupedGrades(
(
await prisma.gradesSection.findMany({
where: constructPrismaQuery(parsedQuery),
include: { instructors: true },
})
).map((section) => ({
...section,
instructors: section.instructors.map((instructor) => instructor.name),
})),
),
headers,
requestId,
);
}
}
return createErrorResult(
400,
Expand Down
47 changes: 45 additions & 2 deletions apps/api/v1/rest/grades/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Prisma } from "@libs/db";
import type { GradeDistribution, GradesAggregate, GradesRaw } from "peterportal-api-next-types";
import type {
AggregateGrades,
AggregateGroupedGradeHeader,
AggregateGroupedGrades,
GradeDistribution,
RawGrades,
} from "peterportal-api-next-types";

import { Query } from "./schema";

Expand All @@ -10,6 +16,8 @@ import { Query } from "./schema";
*/
export const lexOrd = (a: string, b: string): number => (a === b ? 0 : a > b ? 1 : -1);

const headerKeys = ["department", "courseNumber", "instructor"];

const isNotPNPOnly = ({
gradeACount,
gradeBCount,
Expand Down Expand Up @@ -63,7 +71,7 @@ export function constructPrismaQuery(parsedQuery: Query): Prisma.GradesSectionWh
* mean of each section's average GPA in the given dataset.
* @param grades The array of grades to aggregate.
*/
export function aggregateGrades(grades: GradesRaw): GradesAggregate {
export function aggregateGrades(grades: RawGrades): AggregateGrades {
return {
sectionList: grades.map(
({ year, quarter, sectionCode, department, courseNumber, courseNumeric, instructors }) => ({
Expand Down Expand Up @@ -96,3 +104,38 @@ export function aggregateGrades(grades: GradesRaw): GradesAggregate {
},
};
}

/**
* Given an array of sections and their grades distributions, aggregate them into
* an array of objects, such that if two sections have the same department, course number,
* and instructor, they would be grouped together and aggregated into the same object by
* {@link `aggregateGrades`}.
* @param grades The array of grades to aggregate.
*/
export function aggregateGroupedGrades(grades: RawGrades): AggregateGroupedGrades {
const courses = new Map<string, RawGrades>();
for (const grade of grades) {
for (const instructor of grade.instructors) {
const { department, courseNumber } = grade;
const key = JSON.stringify([department, courseNumber, instructor]);
if (courses.has(key)) {
courses.get(key)?.push(grade);
} else {
courses.set(key, [grade]);
}
}
}
return Array.from(courses)
.map(([k, v]) => ({
...(Object.fromEntries(
(JSON.parse(k) as string[]).map((x, i) => [headerKeys[i], x]),
) as AggregateGroupedGradeHeader),
...aggregateGrades(v).gradeDistribution,
}))
.sort(
(a, b) =>
lexOrd(a.department, b.department) ||
lexOrd(a.courseNumber, b.courseNumber) ||
lexOrd(a.instructor, b.instructor),
);
}
108 changes: 108 additions & 0 deletions apps/docs/docs/developers-guide/rest-api/reference/grades.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,111 @@ type GradesOptions = {

</TabItem>
</Tabs>

## Get grade statistics aggregated by course/instructor for certain sections

Formally, if two sections have the same department code, course number, and instructor name, then they will be aggregated together for the purposes of this endpoint. For queries that involve an entire department, this is equivalent to running an aggregate query for each course number-instructor pair, but much faster.

Note that graduate students who are listed as instructors on WebSoc may also be included.

### Code sample

<Tabs>
<TabItem value="bash" label="cURL">

```bash
curl "https://api-next.peterportal.org/v1/rest/grades/aggregateGrouped?year=2023&department=COMPSCI&courseNumber=161"
```

</TabItem>
</Tabs>

### Response

<Tabs>
<TabItem value="json" label="Example response">

```json
[
{
"department": "COMPSCI",
"courseNumber": "161",
"instructor": "FRISHBERG, D.",
"gradeACount": 165,
"gradeBCount": 42,
"gradeCCount": 59,
"gradeDCount": 0,
"gradeFCount": 14,
"gradePCount": 0,
"gradeNPCount": 0,
"gradeWCount": 2,
"averageGPA": 3.23
},
{
"department": "COMPSCI",
"courseNumber": "161",
"instructor": "KALOGIANNIS, F.",
"gradeACount": 165,
"gradeBCount": 42,
"gradeCCount": 59,
"gradeDCount": 0,
"gradeFCount": 14,
"gradePCount": 0,
"gradeNPCount": 0,
"gradeWCount": 2,
"averageGPA": 3.23
},
{
"department": "COMPSCI",
"courseNumber": "161",
"instructor": "PANAGEAS, I.",
"gradeACount": 101,
"gradeBCount": 115,
"gradeCCount": 48,
"gradeDCount": 15,
"gradeFCount": 12,
"gradePCount": 0,
"gradeNPCount": 0,
"gradeWCount": 2,
"averageGPA": 2.935
},
{
"department": "COMPSCI",
"courseNumber": "161",
"instructor": "SHINDLER, M.",
"gradeACount": 165,
"gradeBCount": 42,
"gradeCCount": 59,
"gradeDCount": 0,
"gradeFCount": 14,
"gradePCount": 0,
"gradeNPCount": 0,
"gradeWCount": 2,
"averageGPA": 3.23
}
]
```

</TabItem>
<TabItem value="ts" label="Payload schema">

```typescript
// https://github.com/icssc/peterportal-api-next/blob/main/packages/peterportal-api-next-types/types/grades.ts
type AggregateGroupedGrades = {
department: string;
courseNumber: string;
instructor: string;
gradeACount: number;
gradeBCount: number;
gradeCCount: number;
gradeDCount: number;
gradeFCount: number;
gradePCount: number;
gradeNPCount: number;
gradeWCount: number;
averageGPA: number;
}[];
```

</TabItem>
</Tabs>
20 changes: 16 additions & 4 deletions packages/peterportal-api-next-types/types/grades.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Quarter } from "./constants";
/**
* A section which has grades data associated with it.
*/
export type GradeSection = {
export type GradesSection = {
/**
* The year the section was offered.
*/
Expand Down Expand Up @@ -76,22 +76,24 @@ export type GradeDistribution = {
averageGPA: number;
};

export type RawGrade = GradesSection & GradeDistribution;

/**
* The type of the payload returned on a successful response from querying
* ``/v1/rest/grades/raw``.
*/
export type GradesRaw = (GradeSection & GradeDistribution)[];
export type RawGrades = RawGrade[];

/**
* An object that represents aggregate grades statistics for a given query.
* The type of the payload returned on a successful response from querying
* ``/v1/rest/grades/aggregate``.
*/
export type GradesAggregate = {
export type AggregateGrades = {
/**
* The list of sections in the query.
*/
sectionList: GradeSection[];
sectionList: GradesSection[];
/**
* The combined grades distribution of all sections in the query.
*/
Expand Down Expand Up @@ -124,3 +126,13 @@ export type GradesOptions = {
*/
instructors: string[];
};

export type AggregateGroupedGradeHeader = {
department: string;
courseNumber: string;
instructor: string;
};

export type AggregateGroupedGrade = AggregateGroupedGradeHeader & GradeDistribution;

export type AggregateGroupedGrades = AggregateGroupedGrade[];
2 changes: 2 additions & 0 deletions packages/websoc-fuzzy-search/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { index } from "../output";

import { types, fieldNames, courseFieldNames, instructorFieldNames } from "./constants";
Expand Down