feat: stop calling course blocks rest API and assume LS exists (#803)

- Assume that Learning Sequences is available (waffle has been
  removed)
- Stop calling course blocks API, which provided mostly duplicated
  information now.
- Refactor a bit to avoid needing to globally know which units
  exist in sequences. That is now provided just-in-time for only
  the current sequence.
- Add /first and /last URLs that you can use instead of unit IDs
  in URL paths, in service of the above point.

AA-1040
AA-1153
This commit is contained in:
Michael Terry
2022-02-17 14:10:24 -05:00
committed by GitHub
parent 616027df86
commit 3c52eb2e8d
28 changed files with 256 additions and 499 deletions

View File

@@ -10,6 +10,7 @@ import {
fetchCourse,
fetchSequence,
getResumeBlock,
getSequenceForUnitDeprecated,
saveSequencePosition,
} from './data';
import { TabPage } from '../tab-page';
@@ -17,6 +18,7 @@ import { TabPage } from '../tab-page';
import Course from './course';
import { handleNextSectionCelebration } from './course/celebration';
// Look at where this is called in componentDidUpdate for more info about its usage
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId) => {
if (courseStatus === 'loaded' && !sequenceId) {
// Note that getResumeBlock is just an API call, not a redux thunk.
@@ -31,12 +33,14 @@ const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSe
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
history.replace(`/course/${courseId}/${unitId}`);
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
// If the section is non-empty, redirect to its first sequence.
@@ -49,14 +53,35 @@ const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequence
}
});
const checkUnitToSequenceUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, unit) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && unit) {
// If the sequence failed to load as a sequence, but it *did* load as a unit, then
// insert the unit's parent sequenceId into the URL.
history.replace(`/course/${courseId}/${unit.sequenceId}/${unit.id}`);
// Look at where this is called in componentDidUpdate for more info about its usage
const checkUnitToSequenceUnitRedirect = memoize((
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId,
) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
if (sequenceMightBeUnit) {
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then we need to look up the
// correct parent sequence for it, and redirect there.
const unitId = sequenceId; // just for clarity during the rest of this method
getSequenceForUnitDeprecated(courseId, unitId).then(
parentId => {
if (parentId) {
history.replace(`/course/${courseId}/${parentId}/${unitId}`);
} else {
history.replace(`/course/${courseId}`);
}
},
() => { // error case
history.replace(`/course/${courseId}`);
},
);
} else {
// Invalid sequence that isn't a unit either. Redirect up to main course.
history.replace(`/course/${courseId}`);
}
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
@@ -67,6 +92,33 @@ const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, s
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
if (sequenceStatus !== 'loaded' || !sequence.id) {
return;
}
const hasUnits = sequence.unitIds?.length > 0;
if (unitId === 'first') {
if (hasUnits) {
const firstUnitId = sequence.unitIds[0];
history.replace(`/course/${courseId}/${sequence.id}/${firstUnitId}`);
} else {
// No units... go to general sequence page
history.replace(`/course/${courseId}/${sequence.id}`);
}
} else if (unitId === 'last') {
if (hasUnits) {
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
history.replace(`/course/${courseId}/${sequence.id}/${lastUnitId}`);
} else {
// No units... go to general sequence page
history.replace(`/course/${courseId}/${sequence.id}`);
}
}
});
class CoursewareContainer extends Component {
checkSaveSequencePosition = memoize((unitId) => {
const {
@@ -111,9 +163,9 @@ class CoursewareContainer extends Component {
sequenceId,
courseStatus,
sequenceStatus,
sequenceMightBeUnit,
sequence,
firstSequenceId,
unitViaSequenceId,
sectionViaSequenceId,
match: {
params: {
@@ -128,12 +180,24 @@ class CoursewareContainer extends Component {
this.checkFetchCourse(routeCourseId);
this.checkFetchSequence(routeSequenceId);
// Check if we should save our sequence position. Only do this when the route unit ID changes.
this.checkSaveSequencePosition(routeUnitId);
// Coerce the route ids into null here because they can be undefined, but the redux ids would be null instead.
if (courseId !== (routeCourseId || null) || sequenceId !== (routeSequenceId || null)) {
// The non-route ids are pulled from redux state - they are changed at the same time as the status variables.
// But the route ids are pulled directly from the route. So if the route changes, and we start a fetch above,
// there's a race condition where the route ids are for one course, but the status and the other ids are for a
// different course. Since all the logic below depends on the status variables and the route unit id, we'll wait
// until the ids match and thus the redux states got updated. So just bail for now.
return;
}
// All courseware URLs should normalize to the format /course/:courseId/:sequenceId/:unitId
// via the series of redirection rules below.
// See docs/decisions/0008-liberal-courseware-path-handling.md for more context.
// (It would be ideal to move this logic into the thunks layer and perform
// all URL-changing checks at once. This should be done once the MFE is moved
// to the new Outlines API. See TNL-8182.)
// all URL-changing checks at once. See TNL-8182.)
// Check resume redirect:
// /course/:courseId -> /course/:courseId/:sequenceId/:unitId
@@ -161,16 +225,22 @@ class CoursewareContainer extends Component {
// Check unit to sequence-unit redirect:
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
// by filling in the ID of the parent sequence of :unitId.
checkUnitToSequenceUnitRedirect(courseStatus, courseId, sequenceStatus, unitViaSequenceId);
checkUnitToSequenceUnitRedirect(
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId,
);
// Check to sequence to sequence-unit redirect:
// Check sequence to sequence-unit redirect:
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
// by filling in the ID the most-recently-active unit in the sequence, OR
// the ID of the first unit the sequence if none is active.
checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId);
// Check if we should save our sequence position. Only do this when the route unit ID changes.
this.checkSaveSequencePosition(routeUnitId);
// Check sequence-unit marker to sequence-unit redirect:
// /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId
// /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId
// by filling in the ID the first or last unit in the sequence.
// "Sequence unit marker" is an invented term used only in this component.
checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId);
}
handleUnitNavigationClick = (nextUnitId) => {
@@ -197,18 +267,11 @@ class CoursewareContainer extends Component {
} = this.props;
if (nextSequence !== null) {
let nextUnitId = null;
if (nextSequence.unitIds.length > 0) {
[nextUnitId] = nextSequence.unitIds;
history.push(`/course/${courseId}/${nextSequence.id}/${nextUnitId}`);
} else {
// Some sequences have no units. This will show a blank page with prev/next buttons.
history.push(`/course/${courseId}/${nextSequence.id}`);
}
history.push(`/course/${courseId}/${nextSequence.id}/first`);
const celebrateFirstSection = course && course.celebrations && course.celebrations.firstSection;
if (celebrateFirstSection && sequence.sectionId !== nextSequence.sectionId) {
handleNextSectionCelebration(sequenceId, nextSequence.id, nextUnitId);
handleNextSectionCelebration(sequenceId, nextSequence.id);
}
}
}
@@ -216,13 +279,7 @@ class CoursewareContainer extends Component {
handlePreviousSequenceClick = () => {
const { previousSequence, courseId } = this.props;
if (previousSequence !== null) {
if (previousSequence.unitIds.length > 0) {
const previousUnitId = previousSequence.unitIds[previousSequence.unitIds.length - 1];
history.push(`/course/${courseId}/${previousSequence.id}/${previousUnitId}`);
} else {
// Some sequences have no units. This will show a blank page with prev/next buttons.
history.push(`/course/${courseId}/${previousSequence.id}`);
}
history.push(`/course/${courseId}/${previousSequence.id}/last`);
}
}
@@ -259,18 +316,10 @@ class CoursewareContainer extends Component {
}
}
const unitShape = PropTypes.shape({
id: PropTypes.string.isRequired,
sequenceId: PropTypes.string.isRequired,
});
const sequenceShape = PropTypes.shape({
id: PropTypes.string.isRequired,
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
unitIds: PropTypes.arrayOf(PropTypes.string),
sectionId: PropTypes.string.isRequired,
isTimeLimited: PropTypes.bool,
isProctored: PropTypes.bool,
legacyWebUrl: PropTypes.string,
});
const sectionShape = PropTypes.shape({
@@ -297,9 +346,9 @@ CoursewareContainer.propTypes = {
firstSequenceId: PropTypes.string,
courseStatus: PropTypes.oneOf(['loaded', 'loading', 'failed', 'denied']).isRequired,
sequenceStatus: PropTypes.oneOf(['loaded', 'loading', 'failed']).isRequired,
sequenceMightBeUnit: PropTypes.bool.isRequired,
nextSequence: sequenceShape,
previousSequence: sequenceShape,
unitViaSequenceId: unitShape,
sectionViaSequenceId: sectionShape,
course: courseShape,
sequence: sequenceShape,
@@ -315,7 +364,6 @@ CoursewareContainer.defaultProps = {
firstSequenceId: null,
nextSequence: null,
previousSequence: null,
unitViaSequenceId: null,
sectionViaSequenceId: null,
course: null,
sequence: null,
@@ -398,18 +446,13 @@ const sectionViaSequenceIdSelector = createSelector(
(sectionsById, sequenceId) => (sectionsById[sequenceId] ? sectionsById[sequenceId] : null),
);
const unitViaSequenceIdSelector = createSelector(
(state) => state.models.units || {},
(state) => state.courseware.sequenceId,
(unitsById, sequenceId) => (unitsById[sequenceId] ? unitsById[sequenceId] : null),
);
const mapStateToProps = (state) => {
const {
courseId,
sequenceId,
courseStatus,
sequenceStatus,
sequenceMightBeUnit,
} = state.courseware;
return {
@@ -417,13 +460,13 @@ const mapStateToProps = (state) => {
sequenceId,
courseStatus,
sequenceStatus,
sequenceMightBeUnit,
course: currentCourseSelector(state),
sequence: currentSequenceSelector(state),
previousSequence: previousSequenceSelector(state),
nextSequence: nextSequenceSelector(state),
firstSequenceId: firstSequenceIdSelector(state),
sectionViaSequenceId: sectionViaSequenceIdSelector(state),
unitViaSequenceId: unitViaSequenceIdSelector(state),
};
};