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:
@@ -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 }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user