Skip to content

Commit fd43ec6

Browse files
asadali145ihor-romaniuk
authored andcommitted
fix: load sequences in unit page (openedx#1867) (openedx#2424)
This handles loading errors when opening the course unit page via direct link as an unauthorized user. Co-authored-by: Ihor Romaniuk <[email protected]>
1 parent ae4c4eb commit fd43ec6

12 files changed

Lines changed: 283 additions & 1435 deletions

File tree

src/course-unit/CourseUnit.test.jsx

Lines changed: 206 additions & 194 deletions
Large diffs are not rendered by default.

src/course-unit/__mocks__/courseUnitIndex.js

Lines changed: 0 additions & 1126 deletions
This file was deleted.

src/course-unit/__mocks__/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { default as courseUnitIndexMock } from './courseUnitIndex';
21
export { default as courseSectionVerticalMock } from './courseSectionVertical';
32
export { default as courseUnitMock } from './courseUnit';
43
export { default as courseCreateXblockMock } from './courseCreateXblock';

src/course-unit/breadcrumbs/Breadcrumbs.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {
55
} from '../../testUtils';
66

77
import { executeThunk } from '../../utils';
8-
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
8+
import { getCourseSectionVerticalApiUrl } from '../data/api';
99
import { getApiWaffleFlagsUrl } from '../../data/api';
1010
import { fetchWaffleFlags } from '../../data/thunks';
11-
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
12-
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
11+
import { fetchCourseSectionVerticalData } from '../data/thunk';
12+
import { courseSectionVerticalMock } from '../__mocks__';
1313
import Breadcrumbs from './Breadcrumbs';
1414

1515
let axiosMock;
@@ -43,9 +43,9 @@ describe('<Breadcrumbs />', () => {
4343
reduxStore = mocks.reduxStore;
4444

4545
axiosMock
46-
.onGet(getCourseUnitApiUrl(courseId))
47-
.reply(200, courseUnitIndexMock);
48-
await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
46+
.onGet(getCourseSectionVerticalApiUrl(courseId))
47+
.reply(200, courseSectionVerticalMock);
48+
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
4949
axiosMock
5050
.onGet(getCourseSectionVerticalApiUrl(courseId))
5151
.reply(200, courseSectionVerticalMock);

src/course-unit/course-sequence/sequence-navigation/SequenceNavigation.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const SequenceNavigation = ({
3535

3636
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
3737
const renderUnitButtons = () => {
38-
if (sequence?.unitIds?.length === 0 || unitId === null) {
38+
if (sequence.unitIds.length === 0 || unitId === null) {
3939
return (
4040
<div style={{ flexBasis: '100%', minWidth: 0, borderBottom: 'solid 1px #EAEAEA' }} />
4141
);

src/course-unit/data/api.js

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,13 @@ import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockId
77

88
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
99

10-
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
1110
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
1211
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
1312
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
1413
export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/course/${courseId}?format=concise`;
1514
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
1615
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
1716

18-
/**
19-
* Get course unit.
20-
* @param {string} unitId
21-
* @returns {Promise<Object>}
22-
*/
23-
export async function getCourseUnitData(unitId) {
24-
const { data } = await getAuthenticatedHttpClient()
25-
.get(getCourseUnitApiUrl(unitId));
26-
27-
const result = camelCaseObject(data);
28-
result.readOnly = isUnitReadOnly(result);
29-
return result;
30-
}
31-
3217
/**
3318
* Edit course unit display name.
3419
* @param {string} unitId
@@ -47,15 +32,18 @@ export async function editUnitDisplayName(unitId, displayName) {
4732
}
4833

4934
/**
50-
* Get an object containing course section vertical data.
35+
* Fetch vertical block data from the container_handler endpoint.
5136
* @param {string} unitId
5237
* @returns {Promise<Object>}
5338
*/
54-
export async function getCourseSectionVerticalData(unitId) {
39+
export async function getVerticalData(unitId) {
5540
const { data } = await getAuthenticatedHttpClient()
5641
.get(getCourseSectionVerticalApiUrl(unitId));
5742

58-
return normalizeCourseSectionVerticalData(data);
43+
const courseSectionVerticalData = normalizeCourseSectionVerticalData(data);
44+
courseSectionVerticalData.xblockInfo.readOnly = isUnitReadOnly(courseSectionVerticalData.xblockInfo);
45+
46+
return courseSectionVerticalData;
5947
}
6048

6149
/**

src/course-unit/data/selectors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createSelector } from '@reduxjs/toolkit';
22
import { RequestStatus } from 'CourseAuthoring/data/constants';
33

4-
export const getCourseUnitData = (state) => state.courseUnit.unit;
4+
export const getCourseUnitData = (state) => state.courseUnit.courseSectionVertical.xblockInfo ?? {};
55
export const getCanEdit = (state) => state.courseUnit.canEdit;
66
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
77
export const getCourseUnit = (state) => state.courseUnit;

src/course-unit/data/slice.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ const slice = createSlice({
1212
isTitleEditFormOpen: false,
1313
canEdit: true,
1414
loadingStatus: {
15-
fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS,
1615
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
1716
courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS,
1817
},
19-
unit: {},
2018
courseSectionVertical: {},
2119
courseVerticalChildren: { children: [], isPublished: true },
2220
staticFileNotices: {},
@@ -31,15 +29,6 @@ const slice = createSlice({
3129
},
3230
},
3331
reducers: {
34-
fetchCourseItemSuccess: (state, { payload }) => {
35-
state.unit = payload;
36-
},
37-
updateLoadingCourseUnitStatus: (state, { payload }) => {
38-
state.loadingStatus = {
39-
...state.loadingStatus,
40-
fetchUnitLoadingStatus: payload.status,
41-
};
42-
},
4332
updateQueryPendingStatus: (state, { payload }) => {
4433
state.isQueryPending = payload;
4534
},
@@ -81,12 +70,6 @@ const slice = createSlice({
8170
createUnitXblockLoadingStatus: payload.status,
8271
};
8372
},
84-
addNewUnitStatus: (state, { payload }) => {
85-
state.loadingStatus = {
86-
...state.loadingStatus,
87-
fetchUnitLoadingStatus: payload.status,
88-
};
89-
},
9073
updateCourseVerticalChildren: (state, { payload }) => {
9174
state.courseVerticalChildren = payload;
9275
},
@@ -109,8 +92,6 @@ const slice = createSlice({
10992
});
11093

11194
export const {
112-
fetchCourseItemSuccess,
113-
updateLoadingCourseUnitStatus,
11495
updateSavingStatus,
11596
updateModel,
11697
fetchSequenceRequest,

src/course-unit/data/thunk.js

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import { NOTIFICATION_MESSAGES } from '../../constants';
1010
import { updateModel, updateModels } from '../../generic/model-store';
1111
import { messageTypes } from '../constants';
1212
import {
13-
getCourseUnitData,
1413
editUnitDisplayName,
15-
getCourseSectionVerticalData,
14+
getVerticalData,
1615
createCourseXblock,
1716
getCourseVerticalChildren,
1817
handleCourseUnitVisibilityAndData,
@@ -22,8 +21,6 @@ import {
2221
patchUnitItem,
2322
} from './api';
2423
import {
25-
updateLoadingCourseUnitStatus,
26-
fetchCourseItemSuccess,
2724
updateSavingStatus,
2825
fetchSequenceRequest,
2926
fetchSequenceFailure,
@@ -40,30 +37,13 @@ import {
4037
} from './slice';
4138
import { getNotificationMessage } from './utils';
4239

43-
export function fetchCourseUnitQuery(courseId) {
44-
return async (dispatch) => {
45-
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.IN_PROGRESS }));
46-
47-
try {
48-
const courseUnit = await getCourseUnitData(courseId);
49-
50-
dispatch(fetchCourseItemSuccess(courseUnit));
51-
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));
52-
return true;
53-
} catch (error) {
54-
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.FAILED }));
55-
return false;
56-
}
57-
};
58-
}
59-
6040
export function fetchCourseSectionVerticalData(courseId, sequenceId) {
6141
return async (dispatch) => {
6242
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.IN_PROGRESS }));
6343
dispatch(fetchSequenceRequest({ sequenceId }));
6444

6545
try {
66-
const courseSectionVerticalData = await getCourseSectionVerticalData(courseId);
46+
const courseSectionVerticalData = await getVerticalData(courseId);
6747
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
6848
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
6949
dispatch(updateModel({
@@ -94,8 +74,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
9474
try {
9575
await editUnitDisplayName(itemId, displayName).then(async (result) => {
9676
if (result) {
97-
const courseUnit = await getCourseUnitData(itemId);
98-
const courseSectionVerticalData = await getCourseSectionVerticalData(itemId);
77+
const courseSectionVerticalData = await getVerticalData(itemId);
9978
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
10079
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
10180
dispatch(updateModel({
@@ -107,7 +86,6 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
10786
models: courseSectionVerticalData.units || [],
10887
}));
10988
dispatch(fetchSequenceSuccess({ sequenceId }));
110-
dispatch(fetchCourseItemSuccess(courseUnit));
11189
dispatch(hideProcessingNotification());
11290
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
11391
}
@@ -146,8 +124,8 @@ export function editCourseUnitVisibilityAndData(
146124
if (callback) {
147125
callback();
148126
}
149-
const courseUnit = await getCourseUnitData(blockId);
150-
dispatch(fetchCourseItemSuccess(courseUnit));
127+
const courseSectionVerticalData = await getVerticalData(blockId);
128+
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
151129
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
152130
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
153131
dispatch(hideProcessingNotification());
@@ -174,7 +152,7 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
174152
if (result) {
175153
const formattedResult = camelCaseObject(result);
176154
if (body.category === 'vertical') {
177-
const courseSectionVerticalData = await getCourseSectionVerticalData(formattedResult.locator);
155+
const courseSectionVerticalData = await getVerticalData(formattedResult.locator);
178156
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
179157
}
180158
if (body.stagedContent) {
@@ -194,8 +172,8 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
194172
sendMessageToIframe(messageTypes.addXBlock, { data: result });
195173
}
196174
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
197-
const courseUnit = await getCourseUnitData(currentBlockId);
198-
dispatch(fetchCourseItemSuccess(courseUnit));
175+
const courseSectionVerticalData = await getVerticalData(currentBlockId);
176+
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
199177
}
200178
});
201179
} catch (error) {
@@ -240,8 +218,8 @@ export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
240218
try {
241219
await deleteUnitItem(xblockId);
242220
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
243-
const courseUnit = await getCourseUnitData(itemId);
244-
dispatch(fetchCourseItemSuccess(courseUnit));
221+
const courseSectionVerticalData = await getVerticalData(itemId);
222+
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
245223
dispatch(hideProcessingNotification());
246224
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
247225
} catch (error) {
@@ -259,8 +237,8 @@ export function duplicateUnitItemQuery(itemId, xblockId, callback) {
259237
try {
260238
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
261239
callback(courseKey, locator);
262-
const courseUnit = await getCourseUnitData(itemId);
263-
dispatch(fetchCourseItemSuccess(courseUnit));
240+
const courseSectionVerticalData = await getVerticalData(itemId);
241+
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
264242
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
265243
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
266244
dispatch(hideProcessingNotification());
@@ -316,8 +294,8 @@ export function patchUnitItemQuery({
316294
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
317295
callbackFn(sourceLocator);
318296
try {
319-
const courseUnit = await getCourseUnitData(currentParentLocator);
320-
dispatch(fetchCourseItemSuccess(courseUnit));
297+
const courseSectionVerticalData = await getVerticalData(currentParentLocator);
298+
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
321299
} catch (error) {
322300
handleResponseErrors(error, dispatch, updateSavingStatus);
323301
}
@@ -335,8 +313,8 @@ export function updateCourseUnitSidebar(itemId) {
335313
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
336314

337315
try {
338-
const courseUnit = await getCourseUnitData(itemId);
339-
dispatch(fetchCourseItemSuccess(courseUnit));
316+
const courseSectionVerticalData = await getVerticalData(itemId);
317+
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
340318
dispatch(hideProcessingNotification());
341319
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
342320
} catch (error) {

src/course-unit/header-title/HeaderTitle.test.jsx

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { initializeMockApp } from '@edx/frontend-platform';
88

99
import initializeStore from '../../store';
1010
import { executeThunk } from '../../utils';
11-
import { getCourseUnitApiUrl } from '../data/api';
12-
import { fetchCourseUnitQuery } from '../data/thunk';
13-
import { courseUnitIndexMock } from '../__mocks__';
11+
import { getCourseSectionVerticalApiUrl } from '../data/api';
12+
import { fetchCourseSectionVerticalData } from '../data/thunk';
13+
import { courseSectionVerticalMock } from '../__mocks__';
1414
import HeaderTitle from './HeaderTitle';
1515
import messages from './messages';
1616

@@ -52,9 +52,9 @@ describe('<HeaderTitle />', () => {
5252
store = initializeStore();
5353
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
5454
axiosMock
55-
.onGet(getCourseUnitApiUrl(blockId))
56-
.reply(200, courseUnitIndexMock);
57-
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
55+
.onGet(getCourseSectionVerticalApiUrl(blockId))
56+
.reply(200, courseSectionVerticalMock);
57+
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
5858
});
5959

6060
it('render HeaderTitle component correctly', () => {
@@ -80,14 +80,18 @@ describe('<HeaderTitle />', () => {
8080
// Override mock unit with one sourced from an upstream library
8181
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
8282
axiosMock
83-
.onGet(getCourseUnitApiUrl(blockId))
83+
.onGet(getCourseSectionVerticalApiUrl(blockId))
8484
.reply(200, {
85-
...courseUnitIndexMock,
86-
upstreamInfo: {
87-
upstreamRef: 'lct:org:lib:unit:unit-1',
85+
...courseSectionVerticalMock,
86+
xblock_info: {
87+
...courseSectionVerticalMock.xblock_info,
88+
upstreamInfo: {
89+
...courseSectionVerticalMock.xblock_info.upstreamInfo,
90+
upstreamRef: 'lct:org:lib:unit:unit-1',
91+
},
8892
},
8993
});
90-
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
94+
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
9195

9296
const { getByRole } = renderComponent();
9397

@@ -122,16 +126,19 @@ describe('<HeaderTitle />', () => {
122126

123127
it('displays a visibility message with the selected groups for the unit', async () => {
124128
axiosMock
125-
.onGet(getCourseUnitApiUrl(blockId))
129+
.onGet(getCourseSectionVerticalApiUrl(blockId))
126130
.reply(200, {
127-
...courseUnitIndexMock,
128-
user_partition_info: {
129-
...courseUnitIndexMock.user_partition_info,
130-
selected_partition_index: 1,
131-
selected_groups_label: 'Visibility group 1',
131+
...courseSectionVerticalMock,
132+
xblock_info: {
133+
...courseSectionVerticalMock.xblock_info,
134+
user_partition_info: {
135+
...courseSectionVerticalMock.xblock_info.user_partition_info,
136+
selected_partition_index: 1,
137+
selected_groups_label: 'Visibility group 1',
138+
},
132139
},
133140
});
134-
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
141+
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
135142
const { getByText } = renderComponent();
136143
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
137144
.replace('{selectedGroupsLabel}', 'Visibility group 1');
@@ -143,12 +150,15 @@ describe('<HeaderTitle />', () => {
143150

144151
it('displays a visibility message with the selected groups for some of xblock', async () => {
145152
axiosMock
146-
.onGet(getCourseUnitApiUrl(blockId))
153+
.onGet(getCourseSectionVerticalApiUrl(blockId))
147154
.reply(200, {
148-
...courseUnitIndexMock,
149-
has_partition_group_components: true,
155+
...courseSectionVerticalMock,
156+
xblock_info: {
157+
...courseSectionVerticalMock.xblock_info,
158+
has_partition_group_components: true,
159+
},
150160
});
151-
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
161+
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
152162
const { getByText } = renderComponent();
153163

154164
await waitFor(() => {

0 commit comments

Comments
 (0)