Getting gated content and “goto prereq” working.

Also allowing partially defined URLs - it will fill in the sub section or unit ID if it’s missing.
This commit is contained in:
David Joy
2020-01-13 16:52:19 -05:00
parent d097617feb
commit f756299975
10 changed files with 159 additions and 17 deletions

View File

@@ -29,10 +29,10 @@ subscribe(APP_READY, () => {
<Route
exact
path="/"
render={() => <Link to="/course/course-v1%3AedX%2BDemoX%2BDemo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc">Visit Demo Course</Link>}
/>
<Route path="/course/:courseId/:subSectionId/:unitId" component={LearningSequencePage} />
<Route path="/course/:courseId/:subSectionId" component={LearningSequencePage} />
<Route path="/course/:courseId" component={LearningSequencePage} />
</Switch>
<Footer />

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -6,8 +6,9 @@ import PageLoading from './PageLoading';
import messages from './messages';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import CourseStructureContext from './CourseStructureContext';
import { useLoadCourseStructure } from './data/hooks';
import { useLoadCourseStructure, useMissingSubSectionRedirect } from './data/hooks';
import SubSection from './sub-section/SubSection';
import { history } from '@edx/frontend-platform';
function LearningSequencePage({ match, intl }) {
const {
@@ -18,6 +19,8 @@ function LearningSequencePage({ match, intl }) {
const { blocks, loaded, courseBlockId } = useLoadCourseStructure(courseId);
useMissingSubSectionRedirect(loaded, blocks, courseId, courseBlockId, subSectionId);
return (
<main className="container-fluid d-flex flex-column flex-grow-1">
<CourseStructureContext.Provider value={{
@@ -33,8 +36,8 @@ function LearningSequencePage({ match, intl }) {
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
/>}
{loaded && <CourseBreadcrumbs />}
<SubSection />
{loaded && unitId && <CourseBreadcrumbs />}
{subSectionId && <SubSection />}
</CourseStructureContext.Provider>
</main>

View File

@@ -1,4 +1,5 @@
import { useContext, useMemo, useState, useEffect } from 'react';
import { history } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import CourseStructureContext from '../CourseStructureContext';
@@ -18,6 +19,26 @@ export function useBlockAncestry(blockId) {
}, [blocks, blockId, loaded]);
}
export function useMissingSubSectionRedirect(
loaded,
blocks,
courseId,
courseBlockId,
subSectionId,
) {
useEffect(() => {
if (loaded && !subSectionId) {
const course = blocks[courseBlockId];
const nextSectionId = course.children[0];
const nextSection = blocks[nextSectionId];
const nextSubSectionId = nextSection.children[0];
const nextSubSection = blocks[nextSubSectionId];
const nextUnitId = nextSubSection.children[0];
history.push(`/course/${courseId}/${nextSubSectionId}/${nextUnitId}`);
}
}, [loaded, subSectionId]);
}
export function useLoadCourseStructure(courseId) {
const { authenticatedUser } = useContext(AppContext);
@@ -48,7 +69,7 @@ export function useCurrentCourse() {
export function useCurrentSubSection() {
const { loaded, blocks, subSectionId } = useContext(CourseStructureContext);
return loaded ? blocks[subSectionId] : null;
return loaded && subSectionId ? blocks[subSectionId] : null;
}
export function useCurrentSection() {
@@ -60,7 +81,7 @@ export function useCurrentSection() {
export function useCurrentUnit() {
const { loaded, blocks, unitId } = useContext(CourseStructureContext);
return loaded ? blocks[unitId] : null;
return loaded && unitId ? blocks[unitId] : null;
}

View File

@@ -1,4 +1,5 @@
import React, { useContext } from 'react';
import React, { useContext, Suspense } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import SubSectionNavigation from './SubSectionNavigation';
import CourseStructureContext from '../CourseStructureContext';
@@ -7,22 +8,31 @@ import {
useLoadSubSectionMetadata,
useExamRedirect,
usePersistentUnitPosition,
useMissingUnitRedirect,
} from './data/hooks';
import SubSectionMetadataContext from './SubSectionMetadataContext';
import PageLoading from '../PageLoading';
import messages from './messages';
import { useCurrentUnit } from '../data/hooks';
export default function SubSection() {
const ContentLock = React.lazy(() => import('./content-lock'));
function SubSection({ intl }) {
const {
courseId,
subSectionId,
unitId,
blocks,
} = useContext(CourseStructureContext);
const { metadata } = useLoadSubSectionMetadata(courseId, subSectionId);
const { metadata, loaded } = useLoadSubSectionMetadata(courseId, subSectionId);
usePersistentUnitPosition(courseId, subSectionId, unitId, metadata);
useExamRedirect(metadata, blocks);
const ready = blocks !== null && metadata !== null;
useMissingUnitRedirect(metadata, loaded);
const unit = useCurrentUnit();
const ready = blocks !== null && metadata !== null && unitId && unit;
if (!ready) {
return null;
@@ -34,9 +44,22 @@ export default function SubSection() {
<SubSectionMetadataContext.Provider value={metadata}>
<section className="d-flex flex-column flex-grow-1">
<SubSectionNavigation />
{isGated && <div>This is gated content.</div>}
{!isGated && <Unit id={unitId} unit={blocks[unitId]} />}
{isGated && (
<Suspense fallback={<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.content.lock'])}
/>}
>
<ContentLock />
</Suspense>
)}
{!isGated && <Unit id={unitId} unit={unit} />}
</section>
</SubSectionMetadataContext.Provider>
);
}
SubSection.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(SubSection);

View File

@@ -41,7 +41,7 @@ UnitIcon.propTypes = {
};
export default function SubSectionNavigation() {
const { courseId } = useContext(CourseStructureContext);
const { courseId, unitId } = useContext(CourseStructureContext);
const previousUnit = usePreviousUnit();
const nextUnit = useNextUnit();
@@ -60,6 +60,10 @@ export default function SubSectionNavigation() {
history.push(`/course/${courseId}/${unit.parentId}/${unit.id}`);
});
if (!unitId) {
return null;
}
return (
<nav className="flex-grow-0 d-flex w-100 mb-3">
<Button
@@ -82,8 +86,8 @@ export default function SubSectionNavigation() {
}
function UnitNavigation({ clickHandler }) {
const units = useCurrentSubSectionUnits();
const currentUnit = useCurrentUnit();
const units = useCurrentSubSectionUnits();
const metadata = useContext(SubSectionMetadataContext);
const isGated = metadata.gatedContent.gated;

View File

@@ -0,0 +1,44 @@
import React, { useContext, useCallback } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { history } from '@edx/frontend-platform';
import { Button } from '@edx/paragon';
import SubSectionMetadataContext from '../SubSectionMetadataContext';
import messages from './messages';
import { useCurrentSubSection } from '../../data/hooks';
import CourseStructureContext from '../../CourseStructureContext';
function ContentLock({ intl }) {
const { courseId } = useContext(CourseStructureContext);
const metadata = useContext(SubSectionMetadataContext);
const subSection = useCurrentSubSection();
const handleClick = useCallback(() => {
history.push(`/course/${courseId}/${metadata.gatedContent.prereqId}`);
});
return (
<>
<h3>
<FontAwesomeIcon icon={faLock} />{' '}
{subSection.displayName}
</h3>
<h4>{intl.formatMessage(messages['learn.contentLock.content.locked'])}</h4>
<p>{intl.formatMessage(messages['learn.contentLock.complete.prerequisite'], {
prereqSectionName: metadata.gatedContent.prereqSectionName,
})}
</p>
<p>
<Button className="btn-primary" onClick={handleClick}>{intl.formatMessage(messages['learn.contentLock.goToSection'])}</Button>
</p>
</>
);
}
ContentLock.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ContentLock);

View File

@@ -0,0 +1 @@
export { default } from './ContentLock';

View File

@@ -0,0 +1,21 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'learn.contentLock.content.locked': {
id: 'learn.contentLock.content.locked',
defaultMessage: 'Content Locked',
description: 'Message shown to indicate that a piece of content is unavailable and has a prerequisite.',
},
'learn.contentLock.complete.prerequisite': {
id: 'learn.contentLock.complete.prerequisite',
defaultMessage: "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
description: 'Message shown to indicate which prerequisite the student must complete prior to accessing the locked content. {prereqSectionName} is the name of the prerequisite.',
},
'learn.contentLock.goToSection': {
id: 'learn.contentLock.goToSection',
defaultMessage: 'Go To Prerequisite Section',
description: 'A button users can click that navigates their browser to the prerequisite of this section.',
},
});
export default messages;

View File

@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { camelCaseObject } from '@edx/frontend-platform';
import { useState, useEffect, useContext } from 'react';
import { camelCaseObject, history } from '@edx/frontend-platform';
import { getSubSectionMetadata, saveSubSectionPosition } from './api';
import CourseStructureContext from '../../CourseStructureContext';
export function useLoadSubSectionMetadata(courseId, subSectionId) {
const [metadata, setMetadata] = useState(null);
@@ -9,6 +10,7 @@ export function useLoadSubSectionMetadata(courseId, subSectionId) {
useEffect(() => {
setLoaded(false);
setMetadata(null);
getSubSectionMetadata(courseId, subSectionId).then((data) => {
setMetadata(camelCaseObject(data));
setLoaded(true);
@@ -59,3 +61,15 @@ export function usePersistentUnitPosition(courseId, subSectionId, unitId, subSec
saveSubSectionPosition(courseId, subSectionId, newPosition);
}, [courseId, subSectionId, unitId, subSectionMetadata]);
}
export function useMissingUnitRedirect(metadata, loaded) {
const { courseId, subSectionId, unitId } = useContext(CourseStructureContext);
useEffect(() => {
if (loaded && metadata.itemId === subSectionId && !unitId) {
// Position comes from the server as a 1-indexed array index. Convert it to 0-indexed.
const position = metadata.position - 1;
const nextUnitId = metadata.items[position].id;
history.push(`/course/${courseId}/${subSectionId}/${nextUnitId}`);
}
}, [loaded, metadata, unitId]);
}

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'learn.loading.content.lock': {
id: 'learn.loading.content.lock',
defaultMessage: 'Loading locked content messaging...',
description: 'Message shown when an interface about locked content is being loaded',
},
});
export default messages;