Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ repos:
- "config/keycloak/realms/ol-local-realm.json"
additional_dependencies: ["gibberish-detector"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.12"
rev: "v0.15.14"
hooks:
- id: ruff-format
- id: ruff
Expand Down
10 changes: 10 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Release Notes
=============

Version 0.68.6
--------------

- refactor: make internal editor code more generic (#3344)
- Update dependency pyarrow to v23 [SECURITY] (#3358)
- Task rate limits (#3363)
- [pre-commit.ci] pre-commit autoupdate (#3370)
- Product page: list prices and actual prices (#3346)
- [pre-commit.ci] pre-commit autoupdate (#3352)

Version 0.68.5 (Released May 26, 2026)
--------------

Expand Down
2 changes: 1 addition & 1 deletion frontends/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"ol-test-utilities": "0.0.0"
},
"dependencies": {
"@mitodl/mitxonline-api-axios": "2026.5.1",
"@mitodl/mitxonline-api-axios": "2026.5.14",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.12.2",
"tiny-invariant": "^1.3.3"
Expand Down
42 changes: 0 additions & 42 deletions frontends/api/src/hooks/articles/queries.ts

This file was deleted.

4 changes: 4 additions & 0 deletions frontends/api/src/hooks/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import type { User } from "../../generated/v0/api"
import { userQueries } from "./queries"

enum Permission {
/**
* Controls access to all website_content types (both "news" and "article"
* content_type). Despite the name, this is not limited to article content.
*/
ArticleEditor = "is_article_editor",
Authenticated = "is_authenticated",
LearningPathEditor = "is_learning_path_editor",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { renderHook, waitFor } from "@testing-library/react"

import { setupReactQueryTest } from "../test-utils"
import { articleKeys } from "./queries"
import { websiteContentKeys } from "./queries"
import { setMockResponse, urls, makeRequest } from "../../test-utils"
import { UseQueryResult } from "@tanstack/react-query"
import { articles as factory } from "../../test-utils/factories"
import { websiteContent as factory } from "../../test-utils/factories"
import {
useArticleList,
useArticleDetail,
useArticleCreate,
useArticlePartialUpdate,
useArticleDestroy,
useWebsiteContentList,
useWebsiteContentDetail,
useWebsiteContentCreate,
useWebsiteContentPartialUpdate,
useWebsiteContentDestroy,
} from "./index"

/**
Expand All @@ -28,86 +28,86 @@ const assertApiCalled = async (
expect(result.current.data).toEqual(data)
}

describe("useArticleList", () => {
describe("useWebsiteContentList", () => {
it.each([undefined, { limit: 5 }, { limit: 5, offset: 10 }])(
"Calls the correct API",
async (params) => {
const data = factory.articles({ count: 3 })
const data = factory.websiteContents({ count: 3 })
const url = urls.websiteContent.list(params)
const { wrapper } = setupReactQueryTest()
setMockResponse.get(url, data)
const useTestHook = () => useArticleList(params)
const useTestHook = () => useWebsiteContentList(params)
const { result } = renderHook(useTestHook, { wrapper })
assertApiCalled(result, url, "GET", data)
},
)
})

describe("useArticleDetail", () => {
describe("useWebsiteContentDetail", () => {
it("Calls the correct API", async () => {
const data = factory.article()
const data = factory.websiteContent()
const url = urls.websiteContent.details(data.id)

const { wrapper } = setupReactQueryTest()
setMockResponse.get(url, data)
const useTestHook = () => useArticleDetail(data.id)
const useTestHook = () => useWebsiteContentDetail(data.id)
const { result } = renderHook(useTestHook, { wrapper })

assertApiCalled(result, url, "GET", data)
})
})

describe("Article CRUD", () => {
test("useArticleCreate calls correct API", async () => {
describe("Website Content CRUD", () => {
test("useWebsiteContentCreate calls correct API", async () => {
const url = urls.websiteContent.list()
const data = factory.article()
const { id, ...requestData } = factory.article()
const data = factory.websiteContent()
const { id, ...requestData } = factory.websiteContent()
setMockResponse.post(url, data)

const { wrapper, queryClient } = setupReactQueryTest()
jest.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(useArticleCreate, { wrapper })
const { result } = renderHook(useWebsiteContentCreate, { wrapper })
result.current.mutate(requestData)

await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: articleKeys.listRoot(),
queryKey: websiteContentKeys.listRoot(),
})
})

test("useArticlePartialUpdate calls correct API", async () => {
const article = factory.article()
test("useWebsiteContentPartialUpdate calls correct API", async () => {
const article = factory.websiteContent()
const url = urls.websiteContent.details(article.id)
setMockResponse.patch(url, article)

const { wrapper, queryClient } = setupReactQueryTest()
jest.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(useArticlePartialUpdate, { wrapper })
const { result } = renderHook(useWebsiteContentPartialUpdate, { wrapper })
result.current.mutate(article)

await waitFor(() => expect(result.current.isSuccess).toBe(true))
const { id, ...patchData } = article
expect(makeRequest).toHaveBeenCalledWith("patch", url, patchData)
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: articleKeys.detail(article.id),
queryKey: websiteContentKeys.detail(article.id),
})
})

test("useArticleDestroy calls correct API", async () => {
const { id } = factory.article()
test("useWebsiteContentDestroy calls correct API", async () => {
const { id } = factory.websiteContent()
const url = urls.websiteContent.details(id)
setMockResponse.delete(url, null)

const { wrapper, queryClient } = setupReactQueryTest()
jest.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(useArticleDestroy, { wrapper })
const { result } = renderHook(useWebsiteContentDestroy, { wrapper })
result.current.mutate(id)

await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: articleKeys.listRoot(),
queryKey: websiteContentKeys.listRoot(),
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,52 @@ import type { AxiosProgressEvent } from "axios"

import { websiteContentApi, mediaApi } from "../../clients"
import type {
WebsiteContentApiWebsiteContentListRequest as ArticleListRequest,
WebsiteContent as Article,
WebsiteContentApiWebsiteContentListRequest as WebsiteContentListRequest,
WebsiteContent,
} from "../../generated/v1"
import { articleQueries, articleKeys } from "./queries"
import { websiteContentQueries, websiteContentKeys } from "./queries"

const useArticleList = (
params: ArticleListRequest = {},
const useWebsiteContentList = (
params: WebsiteContentListRequest = {},
opts?: { enabled?: boolean },
) => {
return useQuery({
...articleQueries.list(params),
...websiteContentQueries.list(params),
...opts,
})
}

/**
* Query is disabled if id is undefined.
*/
const useArticleDetail = (id: number | undefined) => {
const useWebsiteContentDetail = (id: number | undefined) => {
return useQuery({
...articleQueries.detail(id ?? -1),
...websiteContentQueries.detail(id ?? -1),
enabled: id !== undefined,
})
}

const useArticleDetailRetrieve = (identifier: string | undefined) => {
const useWebsiteContentDetailRetrieve = (identifier: string | undefined) => {
return useQuery({
...articleQueries.articlesDetailRetrieve(identifier ?? ""),
...websiteContentQueries.websiteContentDetailRetrieve(identifier ?? ""),
enabled: identifier !== undefined,
})
}

const useArticleCreate = () => {
const useWebsiteContentCreate = () => {
const client = useQueryClient()
return useMutation({
mutationFn: (
data: Omit<
Article,
WebsiteContent,
"id" | "user" | "created_on" | "updated_on" | "publish_date"
>,
) =>
websiteContentApi
.websiteContentCreate({ WebsiteContentRequest: data })
.then((response) => response.data),
onSuccess: () => {
client.invalidateQueries({ queryKey: articleKeys.listRoot() })
client.invalidateQueries({ queryKey: websiteContentKeys.listRoot() })
},
})
}
Expand Down Expand Up @@ -97,41 +97,46 @@ export const useMediaUpload = () => {
}
}

const useArticleDestroy = () => {
const useWebsiteContentDestroy = () => {
const client = useQueryClient()
return useMutation({
mutationFn: (id: number) => websiteContentApi.websiteContentDestroy({ id }),
onSuccess: () => {
client.invalidateQueries({ queryKey: articleKeys.listRoot() })
client.invalidateQueries({ queryKey: websiteContentKeys.listRoot() })
},
})
}
const useArticlePartialUpdate = () => {
const useWebsiteContentPartialUpdate = () => {
const client = useQueryClient()
return useMutation({
mutationFn: ({ id, ...data }: Partial<Article> & Pick<Article, "id">) =>
mutationFn: ({
id,
...data
}: Partial<WebsiteContent> & Pick<WebsiteContent, "id">) =>
websiteContentApi
.websiteContentPartialUpdate({
id,
PatchedWebsiteContentRequest: data,
})
.then((response) => response.data),
onSuccess: (article: Article) => {
client.invalidateQueries({ queryKey: articleKeys.detail(article.id) })
const identifier = article.slug || article.id.toString()
onSuccess: (websiteContent: WebsiteContent) => {
client.invalidateQueries({
queryKey: articleKeys.articlesDetailRetrieve(identifier),
queryKey: websiteContentKeys.detail(websiteContent.id),
})
const identifier = websiteContent.slug || websiteContent.id.toString()
client.invalidateQueries({
queryKey: websiteContentKeys.websiteContentDetailRetrieve(identifier),
})
},
})
}

export {
useArticleList,
useArticleDetail,
useArticleCreate,
useArticleDestroy,
useArticlePartialUpdate,
articleQueries,
useArticleDetailRetrieve,
useWebsiteContentList,
useWebsiteContentDetail,
useWebsiteContentCreate,
useWebsiteContentDestroy,
useWebsiteContentPartialUpdate,
websiteContentQueries,
useWebsiteContentDetailRetrieve,
}
45 changes: 45 additions & 0 deletions frontends/api/src/hooks/website_content/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { queryOptions } from "@tanstack/react-query"
import { websiteContentApi } from "../../clients"
import type { WebsiteContentApiWebsiteContentListRequest as WebsiteContentListRequest } from "../../generated/v1"

const websiteContentKeys = {
root: ["website_content"],
listRoot: () => [...websiteContentKeys.root, "list"],
list: (params: WebsiteContentListRequest) => [
...websiteContentKeys.listRoot(),
params,
],
detailRoot: () => [...websiteContentKeys.root, "detail"],
detail: (id: number) => [...websiteContentKeys.detailRoot(), id],
websiteContentDetailRetrieve: (identifier: string) => [
...websiteContentKeys.detailRoot(),
identifier,
],
}

const websiteContentQueries = {
list: (params: WebsiteContentListRequest) =>
queryOptions({
queryKey: websiteContentKeys.list(params),
queryFn: () =>
websiteContentApi.websiteContentList(params).then((res) => res.data),
}),
detail: (id: number) =>
queryOptions({
queryKey: websiteContentKeys.detail(id),
queryFn: () =>
websiteContentApi
.websiteContentRetrieve({ id })
.then((res) => res.data),
}),
websiteContentDetailRetrieve: (identifier: string) =>
queryOptions({
queryKey: websiteContentKeys.websiteContentDetailRetrieve(identifier),
queryFn: () =>
websiteContentApi
.websiteContentDetailRetrieve({ identifier })
.then((res) => res.data),
}),
}

export { websiteContentQueries, websiteContentKeys }
Loading
Loading