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

@@ -1,7 +1,6 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import {
getBlockCompletion,
getCourseBlocks,
getCourseMetadata,
getLearningSequencesOutline,
getSequenceMetadata,
@@ -22,119 +21,15 @@ import {
fetchSequenceFailure,
} from './slice';
/**
* Combines the models from the Course Blocks and Learning Sequences API into a
* new models obj that is returned. Does not mutate the models passed in.
*
* For performance and long term maintainability, we want to switch as much of
* the Courseware MFE to use the Learning Sequences API as possible, and
* eventually remove calls to the Course Blocks API. However, right now, certain
* data still has to come form the Course Blocks API. This function is a
* transitional step to help build out some of the data from the new API, while
* falling back to the Course Blocks API for other things.
*
* Overall performance gains will not be realized until we completely remove
* this call to the Course Blocks API (and the need for this function).
*
* @param {*} learningSequencesModels Normalized model from normalizeLearningSequencesData
* @param {*} courseBlocksModels Normalized model from normalizeBlocks
*/
function mergeLearningSequencesWithCourseBlocks(learningSequencesModels, courseBlocksModels) {
// If there's no Learning Sequences API data yet (not active for this course),
// send back the course blocks model as-is.
if (learningSequencesModels === null) {
return courseBlocksModels;
}
const mergedModels = {
courses: {},
sections: {},
sequences: {},
// Units are now copied over verbatim from Course Blocks API, but they
// should eventually come just-in-time, once the Sequence Metadata API is
// made to be acceptably fast.
units: courseBlocksModels.units,
};
// Top level course information
//
// It is not at all clear to me why courses is a dict when there's only ever
// one course, but I'm not going to make that model change right now.
const lsCourse = Object.values(learningSequencesModels.courses)[0];
const [courseBlockId, courseBlock] = Object.entries(courseBlocksModels.courses)[0];
// The Learning Sequences API never exposes the usage key of the root course
// block, which is used as the key here (instead of the CourseKey). It doesn't
// look like anything actually queries for this value though, and even the
// courseBlocksModels.courses uses the CourseKey as the "id" in the value. So
// I'm imitating the form here to minimize the chance of things breaking, but
// I think we should just forget the keys and replace courses with a singular
// course. I might end up doing that before my refactoring is done here. >_<
mergedModels.courses[courseBlockId] = {
// Learning Sequences API Data
id: lsCourse.id,
title: lsCourse.title,
sectionIds: lsCourse.sectionIds,
hasScheduledContent: lsCourse.hasScheduledContent,
// Still pulling from Course Blocks API
effortActivities: courseBlock.effortActivities,
effortTime: courseBlock.effortTime,
};
// List of Sequences comes from Learning Sequences. Course Blocks will have
// extra sequences that we don't want to display to the user, like ones that
// are empty because all the enclosed units are in user partition groups that
// the user is not a part of (e.g. Verified Track).
Object.entries(learningSequencesModels.sequences).forEach(([sequenceId, sequence]) => {
const blocksSequence = courseBlocksModels.sequences[sequenceId];
mergedModels.sequences[sequenceId] = {
// Learning Sequences API Data
id: sequenceId,
title: sequence.title,
// Still pulling from Course Blocks API Data:
effortActivities: blocksSequence.effortActivities,
effortTime: blocksSequence.effortTime,
legacyWebUrl: blocksSequence.legacyWebUrl,
unitIds: blocksSequence.unitIds,
};
});
// List of Sections comes from Learning Sequences.
Object.entries(learningSequencesModels.sections).forEach(([sectionId, section]) => {
const blocksSection = courseBlocksModels.sections[sectionId];
mergedModels.sections[sectionId] = {
// Learning Sequences API Data
id: sectionId,
title: section.title,
sequenceIds: section.sequenceIds,
courseId: lsCourse.id,
// Still pulling from Course Blocks API Data:
effortActivities: blocksSection.effortActivities,
effortTime: blocksSection.effortTime,
};
// Add back-references to this section for all child sequences.
section.sequenceIds.forEach(childSeqId => {
mergedModels.sequences[childSeqId].sectionId = sectionId;
});
});
return mergedModels;
}
export function fetchCourse(courseId) {
return async (dispatch) => {
dispatch(fetchCourseRequest({ courseId }));
Promise.allSettled([
getCourseMetadata(courseId),
getCourseBlocks(courseId),
getLearningSequencesOutline(courseId),
getCourseHomeCourseMetadata(courseId),
]).then(([
courseMetadataResult,
courseBlocksResult,
learningSequencesOutlineResult,
courseHomeMetadataResult]) => {
if (courseMetadataResult.status === 'fulfilled') {
@@ -154,15 +49,12 @@ export function fetchCourse(courseId) {
}));
}
if (courseBlocksResult.status === 'fulfilled') {
if (learningSequencesOutlineResult.status === 'fulfilled') {
const {
courses, sections, sequences, units,
} = mergeLearningSequencesWithCourseBlocks(
learningSequencesOutlineResult.value,
courseBlocksResult.value,
);
courses, sections, sequences,
} = learningSequencesOutlineResult.value;
// This updates the course with a sectionIds array from the blocks data.
// This updates the course with a sectionIds array from the Learning Sequence data.
dispatch(updateModelsMap({
modelType: 'coursewareMeta',
modelsMap: courses,
@@ -171,31 +63,27 @@ export function fetchCourse(courseId) {
modelType: 'sections',
modelsMap: sections,
}));
// We update for sequences and units because the sequence metadata may have come back first.
// We update for sequences because the sequence metadata may have come back first.
dispatch(updateModelsMap({
modelType: 'sequences',
modelsMap: sequences,
}));
dispatch(updateModelsMap({
modelType: 'units',
modelsMap: units,
}));
}
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
const fetchedCourseHomeMetadata = courseHomeMetadataResult.status === 'fulfilled';
const fetchedBlocks = courseBlocksResult.status === 'fulfilled';
const fetchedOutline = learningSequencesOutlineResult.status === 'fulfilled';
// Log errors for each request if needed. Course block failures may occur
// Log errors for each request if needed. Outline failures may occur
// even if the course metadata request is successful
if (!fetchedBlocks) {
const { response } = courseBlocksResult.reason;
if (!fetchedOutline) {
const { response } = learningSequencesOutlineResult.reason;
if (response && response.status === 403) {
// 403 responses are normal - they happen when the learner is logged out.
// We'll redirect them in a moment to the outline tab by calling fetchCourseDenied() below.
logInfo(courseBlocksResult.reason);
logInfo(learningSequencesOutlineResult.reason);
} else {
logError(courseBlocksResult.reason);
logError(learningSequencesOutlineResult.reason);
}
}
if (!fetchedMetadata) {
@@ -205,7 +93,7 @@ export function fetchCourse(courseId) {
logError(courseHomeMetadataResult.reason);
}
if (fetchedMetadata && fetchedCourseHomeMetadata) {
if (courseHomeMetadataResult.value.courseAccess.hasAccess && fetchedBlocks) {
if (courseHomeMetadataResult.value.courseAccess.hasAccess && fetchedOutline) {
// User has access
dispatch(fetchCourseSuccess({ courseId }));
return;
@@ -251,11 +139,11 @@ export function fetchSequence(sequenceId) {
// Some errors are expected - for example, CoursewareContainer may request sequence metadata for a unit and rely
// on the request failing to notice that it actually does have a unit (mostly so it doesn't have to know anything
// about the opaque key structure). In such cases, the backend gives us a 422.
const isExpected = error.response && error.response.status === 422;
if (!isExpected) {
const sequenceMightBeUnit = error?.response?.status === 422;
if (!sequenceMightBeUnit) {
logError(error);
}
dispatch(fetchSequenceFailure({ sequenceId }));
dispatch(fetchSequenceFailure({ sequenceId, sequenceMightBeUnit }));
}
};
}