Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- 6379:6379

steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- name: update apt
run: sudo apt-get update -y
Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
javascript-tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "^22.0.0"
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- name: Build the Docker image
env:
Expand Down Expand Up @@ -199,7 +199,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
Expand All @@ -223,7 +223,7 @@ jobs:
GENERATOR_OUTPUT_DIR_VC: ./frontends/api/src/generated/v0
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "^22.0.0"
Expand Down Expand Up @@ -262,7 +262,7 @@ jobs:
GENERATOR_OUTPUT_DIR_VC: ./frontends/api/src/generated/v1
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "^22.0.0"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/openapi-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout HEAD
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.head_ref }}
path: head
- name: Checkout BASE
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.base_ref }}
path: base
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
Expand Down
17 changes: 17 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
Release Notes
=============

Version 0.45.4
--------------

- Allow runs with multiple prices to still be considered identical (#2563)
- Only show published runs of courses in /items/ endpoint (#2562)
- Fix featured list channel sorting (#2559)
- remove old canvas problem fields (#2552)
- Canvas - skip TutorProblemFile OCR if content is the same (#2557)
- Refactor price assignment for mitxonline (#2550)
- Update MITxOnline API Client (#2554)
- chore(deps): update nginx docker tag to v1.29.1 (#2549)
- chore(deps): update actions/checkout action to v5 (#2551)
- Certificate page design adjustments (#2547)
- Canvas - resolve visibility of orphaned files (#2548)
- Certificate social media / URL sharing (#2524)
- fix(deps): update django-health-check digest to 592f6a8 (#2527)

Version 0.45.1 (Released September 29, 2025)
--------------

Expand Down
6 changes: 3 additions & 3 deletions frontends/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
"@testing-library/react": "^16.3.0",
"enforce-unique": "^1.3.0",
"jest": "^29.7.0",
"jest-when": "^3.6.0",
"jest-when": "^3.7.0",
"lodash": "^4.17.21",
"ol-test-utilities": "0.0.0"
},
"dependencies": {
"@mitodl/mitxonline-api-axios": "^2025.9.11",
"@mitodl/mitxonline-api-axios": "^2025.9.26",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.6.3"
"axios": "^1.12.2"
}
}
42 changes: 10 additions & 32 deletions frontends/api/src/hooks/learningResources/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
LearningResourcesSearchResponse,
} from "../../generated/v1"
import { queryOptions } from "@tanstack/react-query"
import { hasPosition, randomizeGroups } from "./util"

/* List memberships were previously determined in the learningResourcesApi
* from user_list_parents and learning_path_parents on each resource.
Expand All @@ -37,35 +38,6 @@ export const clearListMemberships = (
learning_path_parents: [],
})

const shuffle = ([...arr]) => {
let m = arr.length
while (m) {
const i = Math.floor(Math.random() * m--)
;[arr[m], arr[i]] = [arr[i], arr[m]]
}
return arr
}

const randomizeResults = ([...results]) => {
const resultsByPosition: {
[position: string]: (LearningResource & { position?: string })[] | undefined
} = {}
const randomizedResults: LearningResource[] = []
results.forEach((result) => {
if (!resultsByPosition[result?.position]) {
resultsByPosition[result?.position] = []
}
resultsByPosition[result?.position ?? ""]?.push(result)
})
Object.keys(resultsByPosition)
.sort()
.forEach((position) => {
const shuffled = shuffle(resultsByPosition[position] ?? [])
randomizedResults.push(...shuffled)
})
return randomizedResults
}

const learningResourceKeys = {
root: ["learning_resources"],
// list
Expand Down Expand Up @@ -183,11 +155,17 @@ const learningResourceQueries = {
queryKey: learningResourceKeys.featured(params),
queryFn: () =>
featuredApi.featuredList(params).then((res) => {
const results = res.data.results
const withPosition = results.filter(hasPosition)
if (withPosition.length !== results.length) {
// Should not happen. The featured API always sets position.
console.warn(
"Some featured results are missing position information.",
)
}
return {
...res.data,
results: randomizeResults(
res.data.results.map(clearListMemberships),
),
results: randomizeGroups(withPosition),
}
}),
}),
Expand Down
97 changes: 97 additions & 0 deletions frontends/api/src/hooks/learningResources/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { faker } from "@faker-js/faker/locale/en"
import { randomizeGroups, hasPosition } from "./util"

faker.seed(12345) // Seed faker for consistent test results
jest
.spyOn(Math, "random")
.mockImplementation(() => faker.number.float({ min: 0, max: 1 }))

describe("randomizeGroups", () => {
it("should group by position and randomize within groups with duplicates", () => {
const items = [
{ id: "a1", position: 1 },
{ id: "a2", position: 1 },
{ id: "b1", position: 12 },
{ id: "b2", position: 12 },
{ id: "c1", position: 2 },
{ id: "c2", position: 2 },
{ id: "d1", position: 3 },
]

const result = randomizeGroups(items)

// Should be grouped by position in numerical order
expect(result[0].position).toBe(1)
expect(result[1].position).toBe(1)
expect(result[2].position).toBe(2)
expect(result[3].position).toBe(2)
expect(result[4].position).toBe(3)
expect(result[5].position).toBe(12)
expect(result[6].position).toBe(12)

// Should contain all items
expect(result).toHaveLength(7)
expect(result.map((item) => item.id).sort()).toEqual([
"a1",
"a2",
"b1",
"b2",
"c1",
"c2",
"d1",
])
})

it("should handle positions greater than 10 correctly (avoid lexicographical sorting)", () => {
const items = [
{ id: "item15", position: 15 },
{ id: "item2", position: 2 },
{ id: "item11", position: 11 },
{ id: "item1", position: 1 },
{ id: "item20", position: 20 },
]

const result = randomizeGroups(items)

// Should be numerically sorted: 1, 2, 11, 15, 20
// NOT lexicographically sorted: 1, 11, 15, 2, 20
const positions = result.map((item) => item.position)
expect(positions).toEqual([1, 2, 11, 15, 20])
})

it("should handle empty array", () => {
const items: Array<{ id: string; position: number }> = []
const result = randomizeGroups(items)
expect(result).toEqual([])
})
})

describe("hasPosition", () => {
it("should return true for objects with non-null position", () => {
const obj = { id: "test", position: 5 }
expect(hasPosition(obj)).toBe(true)
})

it("should return false for objects with null position", () => {
const obj = { id: "test", position: null }
expect(hasPosition(obj)).toBe(false)
})

it("should return true for objects with position 0", () => {
const obj = { id: "test", position: 0 }
expect(hasPosition(obj)).toBe(true)
})

it("should act as a type guard", () => {
const obj: { id: string; position: number | null } = {
id: "test",
position: 5,
}

if (hasPosition(obj)) {
// TypeScript should now know that obj.position is number, not number | null
const position: number = obj.position
expect(position).toBe(5)
}
})
})
58 changes: 58 additions & 0 deletions frontends/api/src/hooks/learningResources/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const hasPosition = <T extends { position: number | null }>(
r: T,
): r is T & { position: number } => r.position !== null

const shuffle = ([...arr]) => {
let m = arr.length
while (m) {
const i = Math.floor(Math.random() * m--)
;[arr[m], arr[i]] = [arr[i], arr[m]]
}
return arr
}

/**
* Randomize a group of ordered items, where item positions might be duplicated.
* Ordering is preserved between groups, but randomized within groups.
*
* E.g., given the items
* [
* { id: 1, position: 1 },
* { id: 2, position: 1 },
* { id: 3, position: 2 },
* { id: 4, position: 3 },
* { id: 5, position: 3 },
* { id: 6, position: 3 },
* ]
*
* The results would be:
* [
* ...items with position 1 in random order...,
* ...items with position 2 in random order...,
* ...items with position 3 in random order...
* ]
*/
const randomizeGroups = <T extends { position: number }>(results: T[]): T[] => {
const resultsByPosition: {
[position: string]: T[] | undefined
} = {}
const randomizedResults: T[] = []
results.forEach((result) => {
const pos = result?.position
if (!resultsByPosition[pos]) {
resultsByPosition[pos] = []
}
resultsByPosition[pos]?.push(result)
})
Object.keys(resultsByPosition)
.sort(
(a, b) => Number(a) - Number(b), // Sort positions numerically
)
.forEach((position) => {
const shuffled = shuffle(resultsByPosition[position] ?? [])
randomizedResults.push(...shuffled)
})
return randomizedResults
}

export { randomizeGroups, hasPosition }
4 changes: 2 additions & 2 deletions frontends/api/src/mitxonline/hooks/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { pagesQueries, getPagesDetail } from "./queries"
import { pagesQueries } from "./queries"

export { pagesQueries, getPagesDetail }
export { pagesQueries }
33 changes: 6 additions & 27 deletions frontends/api/src/mitxonline/hooks/pages/queries.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { queryOptions } from "@tanstack/react-query"
import {
// pagesApi,
axiosInstance,
pagesApi,
} from "../../clients"
import { CoursePageList } from "@mitodl/mitxonline-api-axios/v2"
import { pagesApi } from "../../clients"

const pagesKeys = {
root: ["mitxonline", "pages"],
coursePageDetail: (readableId: string) => [
Expand All @@ -14,33 +10,16 @@ const pagesKeys = {
],
}

const getPagesDetail = async (readableId: string) => {
// TODO: When MITxOnline is published, API client will support readable_id param.
// The API supports it now, just not the client.
// return pagesApi
// .pagesfieldstypecmsCoursePageRetrieve({ readable_id: readableId })
// .then((res) => res.data)
const todo = (
..._params: Parameters<typeof pagesApi.pagesfieldstypecmsCoursePageRetrieve>
) => {}
// @ts-expect-error See above ... This error will trigger when client is updated.
todo({ readable_id: readableId })

const BASE_PATH =
process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL?.replace(/\/+$/, "") ?? ""

const url = `${BASE_PATH}/api/v2/pages/?fields=*&readable_id=${encodeURIComponent(readableId)}&type=cms.CoursePage`
return axiosInstance.get<CoursePageList>(url)
}

const pagesQueries = {
courseDetail: (readableId: string) =>
queryOptions({
queryKey: pagesKeys.coursePageDetail(readableId),
queryFn: async () => {
return getPagesDetail(readableId).then((res) => res.data)
return pagesApi
.pagesfieldstypecmsCoursePageRetrieve({ readable_id: readableId })
.then((res) => res.data)
},
}),
}

export { pagesQueries, pagesKeys, getPagesDetail }
export { pagesQueries, pagesKeys }
Loading
Loading