Skip to content
Draft
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
45 changes: 22 additions & 23 deletions src/course-outline/CourseOutline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ describe('<CourseOutline />', () => {
const { findByLabelText } = renderComponent();

axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
.onPatch(getCourseBlockApiUrl(courseId), {
metadata: {
video_sharing_options: VIDEO_SHARING_OPTIONS.allOff,
},
Expand All @@ -721,7 +721,7 @@ describe('<CourseOutline />', () => {

// The video sharing POST is the one with the expected data.
// Find it by data content rather than assuming a fixed index.
const videoSharingPost = axiosMock.history.post.find(
const videoSharingPost = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes('video_sharing_options'),
);
expect(videoSharingPost).toBeDefined();
Expand All @@ -738,7 +738,7 @@ describe('<CourseOutline />', () => {
renderComponent();

axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
.onPatch(getCourseBlockApiUrl(courseId), {
metadata: {
video_sharing_options: VIDEO_SHARING_OPTIONS.allOff,
},
Expand All @@ -749,7 +749,7 @@ describe('<CourseOutline />', () => {
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
);

const videoSharingPost = axiosMock.history.post.find(
const videoSharingPost = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes('video_sharing_options'),
);
expect(videoSharingPost).toBeDefined();
Expand Down Expand Up @@ -955,7 +955,6 @@ describe('<CourseOutline />', () => {
);
expect(newUnitPost).toBeDefined();
expect(JSON.parse(newUnitPost!.data)).toEqual({
type: COURSE_BLOCK_NAMES.vertical.id,
category: COURSE_BLOCK_NAMES.vertical.id,
parent_locator: subsection.id,
display_name: COURSE_BLOCK_NAMES.vertical.name,
Expand Down Expand Up @@ -1065,7 +1064,7 @@ describe('<CourseOutline />', () => {

axiosMock.reset();
axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
.onPatch(getCourseBlockApiUrl(courseId), {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true,
Expand Down Expand Up @@ -1178,7 +1177,7 @@ describe('<CourseOutline />', () => {
.reply(200, courseLaunchMock);
// Rename-specific handlers
axiosMock
.onPost(getCourseItemApiUrl(item.id))
.onPatch(getCourseItemApiUrl(item.id))
.reply(200, { dummy: 'value' });

// mock section, subsection and unit name and check within the elements.
Expand Down Expand Up @@ -1213,7 +1212,7 @@ describe('<CourseOutline />', () => {
fireEvent.change(editField, { target: { value: newName } });
await user.keyboard('{enter}');
expect(
axiosMock.history.post[axiosMock.history.post.length - 1].data,
axiosMock.history.patch[axiosMock.history.patch.length - 1].data,
).toBe(JSON.stringify({
metadata: {
display_name: newName,
Expand Down Expand Up @@ -1432,7 +1431,7 @@ describe('<CourseOutline />', () => {
).toHaveTextContent(cardHeaderMessages.statusBadgeDraft.defaultMessage);

axiosMock
.onPost(getCourseItemApiUrl(item.id), {
.onPatch(getCourseItemApiUrl(item.id), {
publish: 'make_public',
})
.reply(200, { dummy: 'value' });
Expand Down Expand Up @@ -1500,7 +1499,7 @@ describe('<CourseOutline />', () => {
expect(releaseDatePicker).toHaveValue('08/10/2023');

axiosMock
.onPost(getCourseItemApiUrl(section.id), {
.onPatch(getCourseItemApiUrl(section.id), {
publish: 'republish',
metadata: {
visible_to_staff_only: true,
Expand All @@ -1521,7 +1520,7 @@ describe('<CourseOutline />', () => {
const saveButton = await findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));

const sectionCfgPost = axiosMock.history.post.find(
const sectionCfgPost = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes('visible_to_staff_only'),
);
expect(sectionCfgPost).toBeDefined();
Expand Down Expand Up @@ -1571,7 +1570,7 @@ describe('<CourseOutline />', () => {
};

axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });

const [currentSection] = await findAllByTestId('section-card');
Expand Down Expand Up @@ -1630,7 +1629,7 @@ describe('<CourseOutline />', () => {
await user.click(saveButton);

// verify request
const subCfgPost = axiosMock.history.post.find(
const subCfgPost = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType),
);
expect(subCfgPost).toBeDefined();
Expand Down Expand Up @@ -1698,7 +1697,7 @@ describe('<CourseOutline />', () => {
};

axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });

const [currentSection] = await findAllByTestId('section-card');
Expand Down Expand Up @@ -1775,7 +1774,7 @@ describe('<CourseOutline />', () => {
await user.click(saveButton);

// verify request
const cfgPosta = axiosMock.history.post.find(
const cfgPosta = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType),
);
expect(cfgPosta).toBeDefined();
Expand Down Expand Up @@ -1849,7 +1848,7 @@ describe('<CourseOutline />', () => {
};

axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });

const [currentSection] = await findAllByTestId('section-card');
Expand Down Expand Up @@ -1904,7 +1903,7 @@ describe('<CourseOutline />', () => {
await user.click(saveButton);

// verify request
const cfgPostb = axiosMock.history.post.find(
const cfgPostb = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType),
);
expect(cfgPostb).toBeDefined();
Expand Down Expand Up @@ -1960,7 +1959,7 @@ describe('<CourseOutline />', () => {
};

axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });

const [currentSection] = await findAllByTestId('section-card');
Expand Down Expand Up @@ -2016,7 +2015,7 @@ describe('<CourseOutline />', () => {
await user.click(saveButton);

// verify request
const cfgPostc = axiosMock.history.post.find(
const cfgPostc = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType),
);
expect(cfgPostc).toBeDefined();
Expand Down Expand Up @@ -2071,7 +2070,7 @@ describe('<CourseOutline />', () => {
};

axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });

const [, currentSection] = await findAllByTestId('section-card');
Expand Down Expand Up @@ -2125,7 +2124,7 @@ describe('<CourseOutline />', () => {
await user.click(saveButton);

// verify request
const noSpecialPost = axiosMock.history.post.find(
const noSpecialPost = axiosMock.history.patch.find(
(entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType),
);
expect(noSpecialPost).toBeDefined();
Expand Down Expand Up @@ -2169,7 +2168,7 @@ describe('<CourseOutline />', () => {
const isVisibleToStaffOnly = true;

axiosMock
.onPost(getCourseItemApiUrl(unit.id), {
.onPatch(getCourseItemApiUrl(unit.id), {
publish: 'republish',
metadata: {
visible_to_staff_only: isVisibleToStaffOnly,
Expand Down Expand Up @@ -2297,7 +2296,7 @@ describe('<CourseOutline />', () => {
];

axiosMock
.onPost(getCourseItemApiUrl(section.id), {
.onPatch(getCourseItemApiUrl(section.id), {
publish: 'republish',
metadata: {
highlights,
Expand Down
35 changes: 22 additions & 13 deletions src/course-outline/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,19 @@ export const getCourseLaunchApiUrl = ({

export const getCourseBlockApiUrl = (courseId: string) => {
const formattedCourseId = courseIDtoBlockID(courseId);
return `${getApiBaseUrl()}/xblock/${formattedCourseId}`;
// FC-0118: v1 xblock REST detail URL for the course's root block.
return `${getApiBaseUrl()}/api/contentstore/v1/xblock/${formattedCourseId}/`;
};

export const getCourseReindexApiUrl = (reindexLink: string) => `${getApiBaseUrl()}${reindexLink}`;
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
export const getCourseItemApiUrl = (itemId: string) => `${getXBlockBaseApiUrl()}${itemId}`;
export const getXBlockApiUrl = (blockId: string) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
// FC-0118 (ADR 0028/0037): standardized v1 xblock REST endpoint. Collection URL
// (POST here creates a block); detail URLs carry the trailing slash the DRF
// DefaultRouter requires.
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/xblock/`;
export const getCourseItemApiUrl = (itemId: string) => `${getXBlockBaseApiUrl()}${itemId}/`;
// The outline handler (`/xblock/outline/{id}`) is a separate legacy Studio route,
// NOT part of the v1 REST viewset, so it keeps the legacy `/xblock/` base.
export const getXBlockApiUrl = (blockId: string) => `${getApiBaseUrl()}/xblock/outline/${blockId}`;
export const exportTags = (courseId: string) =>
`${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
export const createDiscussionsTopicsUrl = (courseId: string) =>
Expand Down Expand Up @@ -163,7 +169,7 @@ export async function getCourseLaunch({
*/
export async function enableCourseHighlightsEmails(courseId: string): Promise<object> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseBlockApiUrl(courseId), {
.patch(getCourseBlockApiUrl(courseId), {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true,
Expand Down Expand Up @@ -205,7 +211,7 @@ export async function updateCourseSectionHighlights(
highlights: Array<string>,
): Promise<object> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(sectionId), {
.patch(getCourseItemApiUrl(sectionId), {
publish: 'republish',
metadata: {
highlights,
Expand All @@ -220,7 +226,7 @@ export async function updateCourseSectionHighlights(
*/
export async function publishCourseItem(itemId: string): Promise<CourseItemUpdateResult> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(itemId), {
.patch(getCourseItemApiUrl(itemId), {
publish: 'make_public',
});

Expand All @@ -232,7 +238,7 @@ export async function publishCourseItem(itemId: string): Promise<CourseItemUpdat
*/
export async function configureCourseSection(variables: ConfigureSectionData): Promise<object> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(variables.sectionId), {
.patch(getCourseItemApiUrl(variables.sectionId), {
publish: 'republish',
metadata: {
// The backend expects metadata.visible_to_staff_only to either true or null
Expand Down Expand Up @@ -304,7 +310,7 @@ export async function configureCourseSubsection(
});

const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(itemId), body);
.patch(getCourseItemApiUrl(itemId), body);

return data;
}
Expand All @@ -329,7 +335,8 @@ export async function configureCourseUnit(variables: ConfigureUnitData): Promise
};
const url = getCourseItemApiUrl(variables.unitId);
const { data } = await getAuthenticatedHttpClient()
.post(url, body);
// FC-0118: legacy POST-as-update becomes PATCH (partial_update) on v1.
.patch(url, body);

return camelCaseObject(data);
}
Expand All @@ -342,7 +349,7 @@ export async function editItemDisplayName({ itemId, displayName }: {
displayName: string;
}): Promise<CourseItemUpdateResult> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(itemId), {
.patch(getCourseItemApiUrl(itemId), {
metadata: {
display_name: displayName,
},
Expand Down Expand Up @@ -405,7 +412,9 @@ export async function createCourseXblock({
libraryContentKey,
}: CreateCourseXBlockType) {
const body = {
type,
// FC-0118: `type` is intentionally not sent — the v1 strict serializer does
// not accept it and the backend only reads `category` (which falls back to
// `type` here). `library_content_key` is accepted by the v1 serializer.
boilerplate,
category: category || type,
parent_locator: parentLocator,
Expand Down Expand Up @@ -461,7 +470,7 @@ export async function setVideoSharingOption(
videoSharingOption: string,
): Promise<object> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseBlockApiUrl(courseId), {
.patch(getCourseBlockApiUrl(courseId), {
metadata: {
video_sharing_options: videoSharingOption,
},
Expand Down
11 changes: 0 additions & 11 deletions src/course-outline/outline-sidebar/AddSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,18 @@ describe('AddSidebar', () => {
const unit = await screen.findByRole('button', { name: 'Unit' });
await user.click(section);
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'chapter',
category: 'chapter',
parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course',
displayName: 'Section',
})));
await user.click(subsection);
expect(axiosMock.history.post[1].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'sequential',
category: 'sequential',
parentLocator: lastSection.id,
displayName: 'Subsection',
})));
await user.click(unit);
expect(axiosMock.history.post[2].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'vertical',
category: 'vertical',
parentLocator: lastSubsection.id,
displayName: 'Unit',
Expand All @@ -337,14 +334,12 @@ describe('AddSidebar', () => {
await waitFor(() => expect(axiosMock.history.post.length).toBeGreaterThan(1));
// should add a section first
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'chapter',
category: 'chapter',
parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course',
displayName: 'Section',
})));
// then subsection
expect(axiosMock.history.post[1].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'sequential',
category: 'sequential',
parentLocator: sectionId,
displayName: 'Subsection',
Expand All @@ -365,19 +360,16 @@ describe('AddSidebar', () => {
const subsectionId = 'block-v1:UNIX+UX1+2025_T3+type@sequential+block@sequential234';
const unitId = 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@vertical2133';
const sectionBody = snakeCaseKeys({
type: 'chapter',
category: 'chapter',
parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course',
displayName: 'Section',
});
const subsectionBody = snakeCaseKeys({
type: 'sequential',
category: 'sequential',
parentLocator: sectionId,
displayName: 'Subsection',
});
const unitBody = snakeCaseKeys({
type: 'vertical',
category: 'vertical',
parentLocator: subsectionId,
displayName: 'Unit',
Expand Down Expand Up @@ -421,23 +413,20 @@ describe('AddSidebar', () => {
// first one is unit as per mock
await user.click(addBtns[0]);
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'library_v2',
category: 'vertical',
parentLocator: lastSubsection.id,
libraryContentKey: searchResult.results[0].hits[0].usage_key,
})));
// second one is subsection as per mock
await user.click(addBtns[1]);
expect(axiosMock.history.post[1].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'library_v2',
category: 'sequential',
parentLocator: lastSection.id,
libraryContentKey: searchResult.results[0].hits[1].usage_key,
})));
// third one is section as per mock
await user.click(addBtns[2]);
expect(axiosMock.history.post[2].data).toEqual(JSON.stringify(snakeCaseKeys({
type: 'library_v2',
category: 'chapter',
parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course',
libraryContentKey: searchResult.results[0].hits[2].usage_key,
Expand Down
Loading
Loading