Skip to content

Commit

Permalink
feat: use navigation sequence metadata to disabled prev/next buttons
Browse files Browse the repository at this point in the history
Use navigation_disabled sequence metadata based on Hide From TOC
block field, so the student cannot navigate to another sequences in
the course outline.
https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3853975595/Feature+Enhancement+Proposal+Hide+Sections+from+course+outline
  • Loading branch information
mariajgrimaldi committed Mar 1, 2024
1 parent 54bb72b commit 55e1b5f
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/course-home/data/__snapshots__/redux.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ Object {
"effortTime": 15,
"icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
"navigationDisabled": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true,
"title": "Title of Sequence",
Expand Down
1 change: 1 addition & 0 deletions src/course-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
showLink: !!block.lms_web_url,
title: block.display_name,
navigationDisabled: block.navigation_disabled,
};
break;

Expand Down
19 changes: 12 additions & 7 deletions src/courseware/course/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const Course = ({
const sequence = useModel('sequences', sequenceId);
const section = useModel('sections', sequence ? sequence.sectionId : null);
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
const navigationDisabled = sequence?.navigationDisabled ?? false;

const pageTitleBreadCrumbs = [
sequence,
Expand Down Expand Up @@ -76,13 +77,17 @@ const Course = ({
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
<div className="position-relative d-flex align-items-center mb-4 mt-1">
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
isStaff={isStaff}
unitId={unitId}
/>
{!navigationDisabled && (
<>
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
isStaff={isStaff}
unitId={unitId}
/>
</>
)}
{shouldDisplayChat && (
<>
<Chat
Expand Down
21 changes: 21 additions & 0 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ describe('Course', () => {
);
});

it('removes breadcrumbs when navigation is disabled', async () => {
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', children: [] },
{ courseId: mockData.courseId },
)];
const sequenceMetadata = [Factory.build(
'sequenceMetadata',
{ navigation_disabled: true },
{ courseId: mockData.courseId, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({ sequenceBlocks, sequenceMetadata }, false);
const testData = {
...mockData,
sequenceId: sequenceBlocks[0].id,
onNavigate: jest.fn(),
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
});

it('displays first section celebration modal', async () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { firstSection: true } });
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ const SequenceNavigation = ({
}) => {
const sequence = useModel('sequences', sequenceId);
const {
isFirstUnit, isLastUnit, nextLink, previousLink,
isFirstUnit,
isLastUnit,
nextLink,
previousLink,
navigationDisabledPrevSequence,
navigationDisabledNextSequence,
} = useSequenceNavigationMetadata(sequenceId, unitId);
const {
courseId,
Expand Down Expand Up @@ -68,8 +73,7 @@ const SequenceNavigation = ({
const renderPreviousButton = () => {
const disabled = isFirstUnit;
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;

return (
return !navigationDisabledPrevSequence && (
<Button
variant="link"
className="previous-btn"
Expand All @@ -90,7 +94,7 @@ const SequenceNavigation = ({
const disabled = isLastUnit && !exitActive;
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;

return (
return !navigationDisabledNextSequence && (
<Button
variant="link"
className="next-btn"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,52 @@ describe('Sequence Navigation', () => {
fireEvent.click(screen.getByRole('link', { name: /next/i }));
expect(nextHandler).toHaveBeenCalledTimes(1);
});

it('removes "Previous" for first unit in sequence when navigation is disabled', async () => {
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', children: unitBlocks.map(block => block.id) },
{ courseId: courseMetadata.id },
)];
const sequenceMetadata = [Factory.build(
'sequenceMetadata',
{ navigation_disabled: true },
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({ unitBlocks, sequenceBlocks, sequenceMetadata }, false);
const testData = {
...mockData,
sequenceId: sequenceBlocks[0].id,
onNavigate: jest.fn(),
};
render(<SequenceNavigation {...testData} unitId={unitBlocks[0].id} />, { store: testStore, wrapWithRouter: true });
expect(screen.queryByRole('link', { name: /previous/i })).not.toBeInTheDocument();
});

it('removes "Next" for last unit in sequence when navigation is disabled', async () => {
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', children: unitBlocks.map(block => block.id) },
{ courseId: courseMetadata.id },
)];
const sequenceMetadata = [Factory.build(
'sequenceMetadata',
{ navigation_disabled: true },
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({ unitBlocks, sequenceBlocks, sequenceMetadata }, false);
const testData = {
...mockData,
sequenceId: sequenceBlocks[0].id,
onNavigate: jest.fn(),
};
render(
<SequenceNavigation
{...testData}
unitId={unitBlocks[unitBlocks.length - 1].id}
/>,
{ store: testStore, wrapWithRouter: true },
);
expect(screen.queryByRole('link', { name: /next/i })).not.toBeInTheDocument();
});
});
17 changes: 15 additions & 2 deletions src/courseware/course/sequence/sequence-navigation/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId)

// If we don't know the sequence and unit yet, then assume no.
if (courseStatus !== 'loaded' || sequenceStatus !== 'loaded' || !currentSequenceId || !currentUnitId) {
return { isFirstUnit: false, isLastUnit: false };
return {
isFirstUnit: false,
isLastUnit: false,
navigationDisabledNextSequence: false,
navigationDisabledPrevSequence: false,
};
}

const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
Expand All @@ -25,6 +30,9 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId)
const isLastSequence = sequenceIndex === sequenceIds.length - 1;
const isLastUnitInSequence = unitIndex === sequence.unitIds.length - 1;
const isLastUnit = isLastSequence && isLastUnitInSequence;
const sequenceNavigationDisabled = sequence?.navigationDisabled;
const navigationDisabledPrevSequence = sequenceNavigationDisabled && isFirstUnitInSequence;
const navigationDisabledNextSequence = sequenceNavigationDisabled && isLastUnitInSequence;

const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
Expand Down Expand Up @@ -52,6 +60,11 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId)
}

return {
isFirstUnit, isLastUnit, nextLink, previousLink,
isFirstUnit,
isLastUnit,
nextLink,
previousLink,
navigationDisabledNextSequence,
navigationDisabledPrevSequence,
};
}
1 change: 1 addition & 0 deletions src/courseware/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ function normalizeSequenceMetadata(sequence) {
saveUnitPosition: sequence.save_position,
showCompletion: sequence.show_completion,
allowProctoringOptOut: sequence.allow_proctoring_opt_out,
navigationDisabled: sequence.navigation_disabled,
},
units: sequence.items.map(unit => ({
id: unit.id,
Expand Down

0 comments on commit 55e1b5f

Please sign in to comment.