Skip to content

Commit e2abf22

Browse files
Program Summary (#2590)
* add program rows * fix existing tests * add some tests for programsummary * fix some types * make req_tree more flexible * remove some ts-expect-error
1 parent 4788af3 commit e2abf22

File tree

10 files changed

+674
-116
lines changed

10 files changed

+674
-116
lines changed

frontends/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"dependencies": {
3333
"@mitodl/mitxonline-api-axios": "^2025.10.21",
3434
"@tanstack/react-query": "^5.66.0",
35-
"axios": "^1.12.2"
35+
"axios": "^1.12.2",
36+
"tiny-invariant": "^1.3.3"
3637
}
3738
}

frontends/api/src/mitxonline/test-utils/factories/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,14 @@ import * as programs from "./programs"
44
import * as courses from "./courses"
55
import * as organizations from "./organization"
66
import * as user from "./user"
7+
import * as requirements from "./requirements"
78

8-
export { mitx as enrollment, programs, courses, organizations, user, pages }
9+
export {
10+
mitx as enrollment,
11+
programs,
12+
courses,
13+
organizations,
14+
user,
15+
pages,
16+
requirements,
17+
}

frontends/api/src/mitxonline/test-utils/factories/pages.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -279,32 +279,7 @@ const programPageItem: PartialFactory<ProgramPageItem> = (override) => {
279279
electives: [],
280280
},
281281
},
282-
req_tree: [
283-
{
284-
data: {
285-
node_type: "operator",
286-
operator: "all_of",
287-
operator_value: null,
288-
program: faker.number.int(),
289-
course: null,
290-
title: "Required Courses",
291-
elective_flag: false,
292-
},
293-
id: 1,
294-
},
295-
{
296-
data: {
297-
node_type: "operator",
298-
operator: "min_number_of",
299-
operator_value: "2",
300-
program: faker.number.int(),
301-
course: null,
302-
title: "Elective Courses",
303-
elective_flag: true,
304-
},
305-
id: 2,
306-
},
307-
],
282+
req_tree: [],
308283
page: {
309284
feature_image_src: faker.image.url({
310285
width: 1134,
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type {
2+
V2ProgramRequirement,
3+
V2ProgramRequirementData,
4+
} from "@mitodl/mitxonline-api-axios/v2"
5+
import { V2ProgramRequirementDataNodeTypeEnum } from "@mitodl/mitxonline-api-axios/v2"
6+
7+
import { faker } from "@faker-js/faker/locale/en"
8+
import { UniqueEnforcer } from "enforce-unique"
9+
import invariant from "tiny-invariant"
10+
11+
const uniqueNodeId = new UniqueEnforcer()
12+
const uniqueProgramId = new UniqueEnforcer()
13+
14+
type NodeConstructorOpts = {
15+
id?: number | null
16+
data?: Partial<V2ProgramRequirementData>
17+
}
18+
19+
/**
20+
* Build req_tree data for tests:
21+
*
22+
* ```ts
23+
* const root = new RequirementTreeBuilder()
24+
* const required = root.addOperator({ operator: "all_of" })
25+
* const elective = root.addOperator({ operator: "min_number_of", operator_value: "2" })
26+
*
27+
* required // Two required courses
28+
* .addCourse()
29+
* .addCourse()
30+
*
31+
* elective // Three elective courses, two of which must be completed
32+
* .addCourse()
33+
* .addCourse()
34+
* .addCourse()
35+
*
36+
* const req_tree = root.serialize()
37+
* ```
38+
*
39+
*/
40+
class RequirementTreeBuilder implements V2ProgramRequirement {
41+
children: RequirementTreeBuilder[] | undefined
42+
data: V2ProgramRequirementData
43+
id: number
44+
#root: RequirementTreeBuilder
45+
46+
constructor({ id, data }: NodeConstructorOpts = {}) {
47+
this.id = id ?? uniqueNodeId.enforce(faker.number.int)
48+
this.data = {
49+
// @ts-expect-error Root node is not actually exposed in the API
50+
node_type: "program_root",
51+
course: null,
52+
required_program: null,
53+
program: uniqueProgramId.enforce(faker.number.int),
54+
title: null,
55+
operator: null,
56+
operator_value: null,
57+
elective_flag: false,
58+
...data,
59+
}
60+
this.#root = this
61+
}
62+
63+
addChild(node: RequirementTreeBuilder) {
64+
node.#root = this.#root
65+
if (!this.children) {
66+
this.children = []
67+
}
68+
this.children.push(node)
69+
}
70+
71+
addCourse({
72+
course,
73+
}: Pick<Partial<V2ProgramRequirement["data"]>, "course"> = {}) {
74+
const data: V2ProgramRequirementData = {
75+
node_type: V2ProgramRequirementDataNodeTypeEnum.Course,
76+
course: course ?? uniqueProgramId.enforce(faker.number.int),
77+
program: this.#root.data.program,
78+
required_program: null,
79+
title: null,
80+
operator: null,
81+
operator_value: null,
82+
elective_flag: false,
83+
}
84+
const courseNode = new RequirementTreeBuilder({ data })
85+
this.addChild(courseNode)
86+
return courseNode
87+
}
88+
addOperator(opts: {
89+
operator: "min_number_of" | "all_of"
90+
operator_value?: string
91+
}) {
92+
invariant(opts.operator, "operator is required")
93+
if (opts.operator === "min_number_of") {
94+
invariant(
95+
opts.operator_value &&
96+
!isNaN(Number(opts.operator_value)) &&
97+
Number(opts.operator_value) > 0,
98+
"operator_value is required and must be a positive number when operator is min_number_of",
99+
)
100+
}
101+
const data: V2ProgramRequirementData = {
102+
...opts,
103+
node_type: V2ProgramRequirementDataNodeTypeEnum.Operator,
104+
course: null,
105+
required_program: null,
106+
program: this.#root.data.program,
107+
title: null,
108+
elective_flag: opts.operator === "min_number_of" ? true : false,
109+
}
110+
const operatorNode = new RequirementTreeBuilder({ data })
111+
this.addChild(operatorNode)
112+
return operatorNode
113+
}
114+
115+
addProgram(opts: { program?: number; title?: string } = {}) {
116+
const programId = opts.program ?? uniqueProgramId.enforce(faker.number.int)
117+
const data: V2ProgramRequirementData = {
118+
node_type: "program",
119+
course: null,
120+
program: this.#root.data.program,
121+
required_program: programId,
122+
title: null,
123+
operator: null,
124+
operator_value: null,
125+
elective_flag: false,
126+
}
127+
const programNode = new RequirementTreeBuilder({ data })
128+
this.addChild(programNode)
129+
return programNode
130+
}
131+
132+
serialize(): V2ProgramRequirement {
133+
const node = { id: this.id, data: this.data }
134+
const children = this.children?.map((child) => child.serialize())
135+
return children ? { ...node, children } : node
136+
}
137+
}
138+
139+
export { RequirementTreeBuilder }

0 commit comments

Comments
 (0)