Skip to content

Commit 312fd97

Browse files
gumaercjonkafton
andauthored
add program enrollments api and view program certificate button (#2439)
* add program enrollments api and view program certificate button * fix up mobile display * move back to pre-release axios client package and fix issues with types being renamed * actually, we can go back to the released client package * Small refactor --------- Co-authored-by: Jon Kafton <[email protected]>
1 parent 59abf77 commit 312fd97

File tree

8 files changed

+276
-42
lines changed

8 files changed

+276
-42
lines changed

frontends/api/src/mitxonline/clients.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ProgramsApi,
88
ProgramCertificatesApi,
99
UsersApi,
10+
ProgramEnrollmentsApi,
1011
} from "@mitodl/mitxonline-api-axios/v2"
1112
import axios from "axios"
1213

@@ -51,10 +52,17 @@ const courseRunEnrollmentsApi = new EnrollmentsApi(
5152
axiosInstance,
5253
)
5354

55+
const programEnrollmentsApi = new ProgramEnrollmentsApi(
56+
undefined,
57+
BASE_PATH,
58+
axiosInstance,
59+
)
60+
5461
export {
5562
usersApi,
5663
b2bApi,
5764
courseRunEnrollmentsApi,
65+
programEnrollmentsApi,
5866
programsApi,
5967
programCollectionsApi,
6068
coursesApi,

frontends/api/src/mitxonline/hooks/enrollment/queries.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { queryOptions } from "@tanstack/react-query"
2-
import type { CourseRunEnrollment } from "@mitodl/mitxonline-api-axios/v2"
2+
import type {
3+
CourseRunEnrollment,
4+
UserProgramEnrollmentDetail,
5+
} from "@mitodl/mitxonline-api-axios/v2"
36

4-
import { courseRunEnrollmentsApi } from "../../clients"
7+
import { courseRunEnrollmentsApi, programEnrollmentsApi } from "../../clients"
8+
import { RawAxiosRequestConfig } from "axios"
59

610
const enrollmentKeys = {
711
root: ["mitxonline", "enrollments"],
@@ -10,10 +14,11 @@ const enrollmentKeys = {
1014
"courseRunEnrollments",
1115
"list",
1216
],
13-
programEnrollmentsList: () => [
17+
programEnrollmentsList: (opts?: RawAxiosRequestConfig) => [
1418
...enrollmentKeys.root,
1519
"programEnrollments",
1620
"list",
21+
opts,
1722
],
1823
}
1924

@@ -25,6 +30,15 @@ const enrollmentQueries = {
2530
return courseRunEnrollmentsApi.enrollmentsList().then((res) => res.data)
2631
},
2732
}),
33+
programEnrollmentsList: (opts?: RawAxiosRequestConfig) =>
34+
queryOptions({
35+
queryKey: enrollmentKeys.programEnrollmentsList(opts),
36+
queryFn: async (): Promise<UserProgramEnrollmentDetail[]> => {
37+
return programEnrollmentsApi
38+
.programEnrollmentsList(opts)
39+
.then((res) => res.data)
40+
},
41+
}),
2842
}
2943

3044
export { enrollmentQueries, enrollmentKeys }

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

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,80 @@
11
import { mergeOverrides, makePaginatedFactory } from "ol-test-utilities"
22
import type { PartialFactory } from "ol-test-utilities"
3-
import type { CourseWithCourseRunsSerializerV2 } from "@mitodl/mitxonline-api-axios/v2"
3+
import type {
4+
CourseWithCourseRunsSerializerV2,
5+
V1CourseWithCourseRuns,
6+
} from "@mitodl/mitxonline-api-axios/v2"
47
import { faker } from "@faker-js/faker/locale/en"
58
import { UniqueEnforcer } from "enforce-unique"
69

710
const uniqueCourseId = new UniqueEnforcer()
811
const uniqueCourseRunId = new UniqueEnforcer()
912

13+
const v1Course: PartialFactory<V1CourseWithCourseRuns> = (overrides = {}) => {
14+
const defaults: V1CourseWithCourseRuns = {
15+
id: uniqueCourseId.enforce(() => faker.number.int()),
16+
title: faker.lorem.words(3),
17+
readable_id: faker.lorem.slug(),
18+
next_run_id: faker.number.int(),
19+
departments: [
20+
{
21+
name: faker.company.name(),
22+
},
23+
],
24+
page: {
25+
feature_image_src: faker.image.url(),
26+
page_url: faker.internet.url(),
27+
description: faker.lorem.paragraph(),
28+
live: faker.datatype.boolean(),
29+
length: `${faker.number.int({ min: 1, max: 12 })} weeks`,
30+
effort: `${faker.number.int({ min: 1, max: 10 })} hours/week`,
31+
financial_assistance_form_url: faker.internet.url(),
32+
current_price: faker.number.int({ min: 0, max: 1000 }),
33+
instructors: [
34+
{
35+
name: faker.person.fullName(),
36+
bio: faker.lorem.paragraph(),
37+
},
38+
],
39+
},
40+
programs: null,
41+
courseruns: [
42+
{
43+
id: uniqueCourseRunId.enforce(() => faker.number.int()),
44+
title: faker.lorem.words(3),
45+
start_date: faker.date.future().toISOString(),
46+
end_date: faker.date.future().toISOString(),
47+
enrollment_start: faker.date.past().toISOString(),
48+
enrollment_end: faker.date.future().toISOString(),
49+
expiration_date: faker.date.future().toISOString(),
50+
courseware_url: faker.internet.url(),
51+
courseware_id: faker.string.uuid(),
52+
certificate_available_date: faker.date.future().toISOString(),
53+
upgrade_deadline: faker.date.future().toISOString(),
54+
is_upgradable: faker.datatype.boolean(),
55+
is_enrollable: faker.datatype.boolean(),
56+
is_archived: faker.datatype.boolean(),
57+
is_self_paced: faker.datatype.boolean(),
58+
run_tag: faker.lorem.word(),
59+
live: faker.datatype.boolean(),
60+
course_number: faker.lorem.word(),
61+
products: [
62+
{
63+
id: faker.number.int(),
64+
price: faker.commerce.price(),
65+
description: faker.lorem.sentence(),
66+
is_active: faker.datatype.boolean(),
67+
product_flexible_price: null,
68+
},
69+
],
70+
approved_flexible_price_exists: faker.datatype.boolean(),
71+
},
72+
],
73+
}
74+
75+
return mergeOverrides<V1CourseWithCourseRuns>(defaults, overrides)
76+
}
77+
1078
const course: PartialFactory<CourseWithCourseRunsSerializerV2> = (
1179
overrides = {},
1280
) => {
@@ -91,6 +159,7 @@ const course: PartialFactory<CourseWithCourseRunsSerializerV2> = (
91159
return mergeOverrides<CourseWithCourseRunsSerializerV2>(defaults, overrides)
92160
}
93161

162+
const v1Courses = makePaginatedFactory(v1Course)
94163
const courses = makePaginatedFactory(course)
95164

96-
export { course, courses }
165+
export { v1Course, v1Courses, course, courses }

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type { PartialFactory } from "ol-test-utilities"
44
import type {
55
CourseRunEnrollment,
66
CourseRunGrade,
7+
UserProgramEnrollmentDetail,
78
} from "@mitodl/mitxonline-api-axios/v2"
89
import { UniqueEnforcer } from "enforce-unique"
10+
import { factories } from ".."
911

1012
const uniqueEnrollmentId = new UniqueEnforcer()
1113
const uniqueRunId = new UniqueEnforcer()
@@ -99,9 +101,56 @@ const courseEnrollment: PartialFactory<CourseRunEnrollment> = (
99101
return mergeOverrides<CourseRunEnrollment>(defaults, overrides)
100102
}
101103

104+
const programEnrollment: PartialFactory<UserProgramEnrollmentDetail> = (
105+
overrides = {},
106+
): UserProgramEnrollmentDetail => {
107+
const defaults: UserProgramEnrollmentDetail = {
108+
certificate: faker.datatype.boolean()
109+
? {
110+
uuid: faker.string.uuid(),
111+
link: faker.internet.url(),
112+
}
113+
: null,
114+
program: {
115+
id: faker.number.int(),
116+
title: faker.lorem.words(3),
117+
readable_id: faker.lorem.slug(),
118+
courses: factories.courses.v1Course(),
119+
requirements: {
120+
required: [faker.number.int()],
121+
electives: [faker.number.int()],
122+
},
123+
req_tree: [],
124+
page: {
125+
feature_image_src: faker.image.url(),
126+
page_url: faker.internet.url(),
127+
financial_assistance_form_url: faker.internet.url(),
128+
description: faker.lorem.paragraph(),
129+
live: faker.datatype.boolean(),
130+
length: `${faker.number.int({ min: 1, max: 12 })} weeks`,
131+
effort: `${faker.number.int({ min: 1, max: 10 })} hours/week`,
132+
price: faker.commerce.price(),
133+
},
134+
program_type: faker.helpers.arrayElement([
135+
"certificate",
136+
"degree",
137+
"diploma",
138+
]),
139+
departments: [
140+
{
141+
name: faker.company.name(),
142+
},
143+
],
144+
live: faker.datatype.boolean(),
145+
},
146+
enrollments: [courseEnrollment()],
147+
}
148+
return mergeOverrides<UserProgramEnrollmentDetail>(defaults, overrides)
149+
}
150+
102151
// Not paginated
103152
const courseEnrollments = (count: number): CourseRunEnrollment[] => {
104153
return new Array(count).fill(null).map(() => courseEnrollment())
105154
}
106155

107-
export { courseEnrollment, courseEnrollments, grade }
156+
export { courseEnrollment, courseEnrollments, grade, programEnrollment }

frontends/api/src/mitxonline/test-utils/urls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const enrollment = {
1919
`${API_BASE_URL}/api/v1/enrollments/${id ? `${id}/` : ""}`,
2020
}
2121

22+
const programEnrollments = {
23+
enrollmentsList: () => `${API_BASE_URL}/api/v1/program_enrollments/`,
24+
}
25+
2226
const b2b = {
2327
courseEnrollment: (readableId?: string) =>
2428
`${API_BASE_URL}/api/v0/b2b/enroll/${readableId}/`,
@@ -59,4 +63,5 @@ export {
5963
programCollections,
6064
courses,
6165
organization,
66+
programEnrollments,
6267
}

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import moment from "moment"
1414
const makeCourses = factories.courses.courses
1515
const makeProgram = factories.programs.program
1616
const makeProgramCollection = factories.programs.programCollection
17-
const makeEnrollment = factories.enrollment.courseEnrollment
17+
const makeCourseEnrollment = factories.enrollment.courseEnrollment
1818
const makeGrade = factories.enrollment.grade
1919

2020
const dashboardCourse: PartialFactory<DashboardCourse> = (...overrides) => {
@@ -45,20 +45,20 @@ const dashboardCourse: PartialFactory<DashboardCourse> = (...overrides) => {
4545

4646
const setupEnrollments = (includeExpired: boolean) => {
4747
const completed = [
48-
makeEnrollment({
48+
makeCourseEnrollment({
4949
run: { title: "C Course Ended" },
5050
grades: [makeGrade({ passed: true })],
5151
}),
5252
]
5353
const expired = includeExpired
5454
? [
55-
makeEnrollment({
55+
makeCourseEnrollment({
5656
run: {
5757
title: "A Course Ended",
5858
end_date: faker.date.past().toISOString(),
5959
},
6060
}),
61-
makeEnrollment({
61+
makeCourseEnrollment({
6262
run: {
6363
title: "B Course Ended",
6464
end_date: faker.date.past().toISOString(),
@@ -67,26 +67,26 @@ const setupEnrollments = (includeExpired: boolean) => {
6767
]
6868
: []
6969
const started = [
70-
makeEnrollment({
70+
makeCourseEnrollment({
7171
run: {
7272
title: "A Course Started",
7373
start_date: faker.date.past().toISOString(),
7474
},
7575
}),
76-
makeEnrollment({
76+
makeCourseEnrollment({
7777
run: {
7878
title: "B Course Started",
7979
start_date: faker.date.past().toISOString(),
8080
},
8181
}),
8282
]
8383
const notStarted = [
84-
makeEnrollment({
84+
makeCourseEnrollment({
8585
run: {
8686
start_date: moment().add(1, "day").toISOString(), // Sooner first
8787
},
8888
}),
89-
makeEnrollment({
89+
makeCourseEnrollment({
9090
run: {
9191
start_date: moment().add(5, "day").toISOString(), // Later second
9292
},

frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ describe("OrganizationContent", () => {
2424
mockedUseFeatureFlagEnabled.mockReturnValue(true)
2525
// Set default empty enrollments for all tests
2626
setMockResponse.get(urls.enrollment.enrollmentsList(), [])
27+
// Add missing program enrollments mock
28+
setMockResponse.get(urls.programEnrollments.enrollmentsList(), [])
2729
})
2830

2931
it("displays a header for each program returned and cards for courses in program", async () => {
@@ -300,4 +302,41 @@ describe("OrganizationContent", () => {
300302
)
301303
})
302304
})
305+
306+
test("Shows the program certificate link button if the program has a certificate", async () => {
307+
const { orgX, programA } = setupProgramsAndCourses()
308+
309+
// Mock the program to have a certificate
310+
const programWithCertificate = {
311+
...programA,
312+
program_type: "Program", // Set specific program type
313+
certificate: {
314+
uuid: "cert-123",
315+
url: "/certificates/program/1",
316+
},
317+
}
318+
const programEnrollment = factories.enrollment.programEnrollment({
319+
program: { id: programWithCertificate.id },
320+
certificate: {
321+
link: programWithCertificate.certificate.url,
322+
},
323+
})
324+
setMockResponse.get(urls.programs.programsList({ org_id: orgX.id }), {
325+
results: [programWithCertificate],
326+
})
327+
setMockResponse.get(urls.programEnrollments.enrollmentsList(), [
328+
programEnrollment,
329+
])
330+
331+
renderWithProviders(<OrganizationContent orgSlug={orgX.slug} />)
332+
333+
const programRoot = await screen.findByTestId("org-program-root")
334+
const certificateButton = within(programRoot).getByRole("link", {
335+
name: "View Program Certificate",
336+
})
337+
expect(certificateButton).toHaveAttribute(
338+
"href",
339+
programWithCertificate.certificate.url,
340+
)
341+
})
303342
})

0 commit comments

Comments
 (0)