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,3 +1,4 @@
import './courseMetadata.factory';
import './sequenceMetadata.factory';
import './courseRecommendations.factory';
import './learningSequencesOutline.factory';

View File

@@ -29,6 +29,10 @@ export function buildEmptyOutline(courseId) {
// Given courseBlocks (output from buildSimpleCourseBlocks), create a matching
// Learning Sequences API outline (what the REST API would return to us).
// Ideally this method would go away at some point, and we'd use a learning
// sequence factory directly. But this method exists as a bridge-gap from
// when course blocks were always used anyway. Now that they are rarely used,
// this can probably go away.
export function buildOutlineFromBlocks(courseBlocks) {
const sections = {};
const sequences = {};
@@ -40,14 +44,14 @@ export function buildOutlineFromBlocks(courseBlocks) {
} else if (block.type === 'chapter') {
sections[block.id] = {
id: block.id,
title: block.title,
title: block.display_name,
start: null,
sequence_ids: [...block.children],
};
} else if (block.type === 'sequential') {
sequences[block.id] = {
id: block.id,
title: block.title,
title: block.display_name,
accessible: true,
start: null,
};

View File

@@ -1,96 +1,8 @@
import { getConfig, camelCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logInfo } from '@edx/frontend-platform/logging';
import { getTimeOffsetMillis } from '../../course-home/data/api';
import { appendBrowserTimezoneToUrl } from '../../utils';
export function normalizeBlocks(courseId, blocks) {
const models = {
courses: {},
sections: {},
sequences: {},
units: {},
};
Object.values(blocks).forEach(block => {
switch (block.type) {
case 'course':
models.courses[block.id] = {
id: courseId,
title: block.display_name,
sectionIds: block.children || [],
hasScheduledContent: block.has_scheduled_content || false,
};
break;
case 'chapter':
models.sections[block.id] = {
id: block.id,
title: block.display_name,
sequenceIds: block.children || [],
};
break;
case 'sequential':
models.sequences[block.id] = {
effortActivities: block.effort_activities,
effortTime: block.effort_time,
id: block.id,
title: block.display_name,
legacyWebUrl: block.legacy_web_url,
unitIds: block.children || [],
};
break;
case 'vertical':
models.units[block.id] = {
graded: block.graded,
id: block.id,
title: block.display_name,
legacyWebUrl: block.legacy_web_url,
};
break;
default:
logInfo(`Unexpected course block type: ${block.type} with ID ${block.id}. Expected block types are course, chapter, sequential, and vertical.`);
}
});
// Next go through each list and use their child lists to decorate those children with a
// reference back to their parent.
Object.values(models.courses).forEach(course => {
if (Array.isArray(course.sectionIds)) {
course.sectionIds.forEach(sectionId => {
const section = models.sections[sectionId];
section.courseId = course.id;
});
}
});
Object.values(models.sections).forEach(section => {
if (Array.isArray(section.sequenceIds)) {
section.sequenceIds.forEach(sequenceId => {
if (sequenceId in models.sequences) {
models.sequences[sequenceId].sectionId = section.id;
} else {
logInfo(`Section ${section.id} has child block ${sequenceId}, but that block is not in the list of sequences.`);
}
});
}
});
Object.values(models.sequences).forEach(sequence => {
if (Array.isArray(sequence.unitIds)) {
sequence.unitIds.forEach(unitId => {
if (unitId in models.units) {
models.units[unitId].sequenceId = sequence.id;
} else {
logInfo(`Sequence ${sequence.id} has child block ${unitId}, but that block is not in the list of units.`);
}
});
}
});
return models;
}
export function normalizeLearningSequencesData(learningSequencesData) {
const models = {
courses: {},
@@ -108,14 +20,16 @@ export function normalizeLearningSequencesData(learningSequencesData) {
models.sequences[seqId] = {
id: seqId,
title: sequence.title,
legacyWebUrl: `${getConfig().LMS_BASE_URL}/courses/${learningSequencesData.course_key}/jump_to/${seqId}?experience=legacy`,
};
});
// Sections
learningSequencesData.outline.sections.forEach(section => {
// Skipping sections with only inaccessible sequences replicates the behavior of the legacy course blocks API
// (But keep it if it was already empty, again to replicate legacy blocks API.)
const accessibleSequenceIds = section.sequence_ids.filter(seqId => seqId in models.sequences);
if (accessibleSequenceIds.length === 0) {
if (section.sequence_ids.length > 0 && accessibleSequenceIds.length === 0) {
return;
}
@@ -123,7 +37,13 @@ export function normalizeLearningSequencesData(learningSequencesData) {
id: section.id,
title: section.title,
sequenceIds: accessibleSequenceIds,
courseId: learningSequencesData.course_key,
};
// Add back-references to this section for all child sequences.
accessibleSequenceIds.forEach(childSeqId => {
models.sequences[childSeqId].sectionId = section.id;
});
});
// Course
@@ -144,39 +64,24 @@ export function normalizeLearningSequencesData(learningSequencesData) {
return models;
}
export async function getCourseBlocks(courseId) {
// Do not add further calls to this API - we don't like making use of the modulestore if we can help it
export async function getSequenceForUnitDeprecated(courseId, unitId) {
const authenticatedUser = getAuthenticatedUser();
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/`);
url.searchParams.append('course_id', courseId);
url.searchParams.append('username', authenticatedUser ? authenticatedUser.username : '');
url.searchParams.append('depth', 3);
url.searchParams.append('requested_fields', 'children,effort_activities,effort_time,show_gated_sections,graded,special_exam_info,has_scheduled_content');
url.searchParams.append('requested_fields', 'children');
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
return normalizeBlocks(courseId, data.blocks);
const parent = Object.values(data.blocks).find(block => block.type === 'sequential' && block.children.includes(unitId));
return parent?.id;
}
// Returns the output of the Learning Sequences API, or null if that API is not
// currently available for this user in this course.
export async function getLearningSequencesOutline(courseId) {
const outlineUrl = new URL(`${getConfig().LMS_BASE_URL}/api/learning_sequences/v1/course_outline/${courseId}`);
try {
const { data } = await getAuthenticatedHttpClient().get(outlineUrl.href, {});
return normalizeLearningSequencesData(data);
} catch (error) {
// This is not a critical API to use at the moment. If it errors for any
// reason, just send back a null so the higher layers know to ignore it.
if (error.response) {
if (error.response.status === 403) {
logInfo('Learning Sequences API not enabled for this user.');
} else {
logInfo(`Unexpected error calling Learning Sequences API (${error.response.status}). Ignoring.`);
}
return null;
}
throw error;
}
const { data } = await getAuthenticatedHttpClient().get(outlineUrl.href, {});
return normalizeLearningSequencesData(data);
}
function normalizeTabUrls(id, tabs) {

View File

@@ -7,6 +7,7 @@ export {
} from './thunks';
export {
getResumeBlock,
getSequenceForUnitDeprecated,
sendActivationEmail,
} from './api';
export {

View File

@@ -1,10 +1,8 @@
import { Pact, Matchers } from '@pact-foundation/pact';
import path from 'path';
import { mergeConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
getCourseBlocks,
getCourseMetadata,
getLearningSequencesOutline,
getSequenceMetadata,
@@ -33,7 +31,6 @@ const provider = new Pact({
});
describe('Courseware Service', () => {
let authenticatedUser;
beforeAll(async () => {
initializeMockApp();
await provider
@@ -41,62 +38,11 @@ describe('Courseware Service', () => {
.then((options) => mergeConfig({
LMS_BASE_URL: `http://localhost:${options.port}`,
}, 'Custom app config for pact tests'));
authenticatedUser = getAuthenticatedUser();
});
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
describe('When a request to get course blocks is made', () => {
it('returns normalized course blocks', async () => {
await provider.addInteraction({
state: `Blocks data exists for course_id ${courseId}`,
uponReceiving: 'a request to get course blocks',
withRequest: {
method: 'GET',
path: '/api/courses/v2/blocks/',
query: {
course_id: courseId,
username: authenticatedUser ? authenticatedUser.username : '',
depth: '3',
requested_fields: 'children,effort_activities,effort_time,show_gated_sections,graded,special_exam_info,has_scheduled_content',
},
},
willRespondWith: {
status: 200,
body: {
root: string('block-v1:edX+DemoX+Demo_Course+type@course+block@course'),
blocks: like({
'block-v1:edX+DemoX+Demo_Course+type@course+block@course': {
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
block_id: 'course',
lms_web_url: '/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@course+block@course',
legacy_web_url: '/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@course+block@course?experience=legacy',
student_view_url: '/xblock/block-v1:edX+DemoX+Demo_Course+type@course+block@course',
type: 'course',
display_name: 'Demonstration Course',
},
}),
},
},
});
const normalizedCourseBlock = {
'block-v1:edX+DemoX+Demo_Course+type@course+block@course': {
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demonstration Course',
sectionIds: [],
hasScheduledContent: false,
},
};
const response = await getCourseBlocks(courseId);
expect(response).toBeTruthy();
expect(response.courses).toEqual(normalizedCourseBlock);
expect(response.sections).toEqual({});
expect(response.sequences).toEqual({});
expect(response.units).toEqual({});
});
});
describe('When a request to get a learning sequence outline is made', () => {
it('returns a normalized outline', async () => {
await provider.addInteraction({
@@ -109,7 +55,7 @@ describe('Courseware Service', () => {
willRespondWith: {
status: 200,
body: {
course_key: string('block-v1:edX+DemoX+Demo_Course'),
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: {
sections: [],
@@ -120,8 +66,8 @@ describe('Courseware Service', () => {
});
const normalizedOutline = {
courses: {
'block-v1:edX+DemoX+Demo_Course': {
id: 'block-v1:edX+DemoX+Demo_Course',
'course-v1:edX+DemoX+Demo_Course': {
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demo Course',
sectionIds: [],
hasScheduledContent: false,
@@ -145,7 +91,7 @@ describe('Courseware Service', () => {
willRespondWith: {
status: 200,
body: {
course_key: string('block-v1:edX+DemoX+Demo_Course'),
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: like({
sections: [
@@ -191,8 +137,8 @@ describe('Courseware Service', () => {
});
const normalizedOutline = {
courses: {
'block-v1:edX+DemoX+Demo_Course': {
id: 'block-v1:edX+DemoX+Demo_Course',
'course-v1:edX+DemoX+Demo_Course': {
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demo Course',
sectionIds: [
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
@@ -204,6 +150,7 @@ describe('Courseware Service', () => {
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial': {
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially accessible',
courseId: 'course-v1:edX+DemoX+Demo_Course',
sequenceIds: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
],
@@ -213,6 +160,8 @@ describe('Courseware Service', () => {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
legacyWebUrl: `${getConfig().LMS_BASE_URL}/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible?experience=legacy`,
},
},
};

View File

@@ -19,7 +19,6 @@ const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
describe('Data layer integration tests', () => {
const courseBaseUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course`;
const courseBlocksUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/*`);
const learningSequencesUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/learning_sequences/v1/course_outline/*`);
const sequenceBaseUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence`;
@@ -57,7 +56,6 @@ describe('Data layer integration tests', () => {
describe('Test fetchCourse', () => {
it('Should fail to fetch course and blocks if request error happens', async () => {
axiosMock.onGet(courseUrl).networkError();
axiosMock.onGet(courseBlocksUrlRegExp).networkError();
axiosMock.onGet(learningSequencesUrlRegExp).networkError();
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
@@ -87,8 +85,7 @@ describe('Data layer integration tests', () => {
axiosMock.onGet(forbiddenCourseHomeUrl).reply(200, forbiddenCourseHomeMetadata);
axiosMock.onGet(forbiddenCourseUrl).reply(200, forbiddenCourseMetadata);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, forbiddenCourseBlocks);
axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {});
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(forbiddenCourseBlocks));
await executeThunk(thunks.fetchCourse(forbiddenCourseMetadata.id), store.dispatch);
@@ -103,8 +100,7 @@ describe('Data layer integration tests', () => {
it('Should fetch, normalize, and save metadata', async () => {
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {});
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
@@ -124,7 +120,6 @@ describe('Data layer integration tests', () => {
// (even though it won't actually filter down in this case).
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, simpleOutline);
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
@@ -148,7 +143,6 @@ describe('Data layer integration tests', () => {
// (even though it won't actually filter down in this case).
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
// Create an outline with basic matching metadata, but then empty it out...
const emptyOutline = buildOutlineFromBlocks(courseBlocks);
@@ -201,8 +195,7 @@ describe('Data layer integration tests', () => {
it('Should fetch and normalize metadata, and then update existing models with sequence metadata', async () => {
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {});
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
axiosMock.onGet(sequenceUrl).reply(200, sequenceMetadata);
// setting course with blocks before sequence to check that blocks receive
@@ -217,12 +210,6 @@ describe('Data layer integration tests', () => {
activeUnitIndex: expect.any(Number),
}),
});
expect(state.models.units).toEqual({
[unitId]: expect.not.objectContaining({
complete: null,
bookmarked: expect.any(Boolean),
}),
});
// Update our state variable again.
state = store.getState();

View File

@@ -13,6 +13,7 @@ const slice = createSlice({
courseId: null,
sequenceStatus: 'loading',
sequenceId: null,
sequenceMightBeUnit: false,
},
reducers: {
fetchCourseRequest: (state, { payload }) => {
@@ -34,14 +35,17 @@ const slice = createSlice({
fetchSequenceRequest: (state, { payload }) => {
state.sequenceId = payload.sequenceId;
state.sequenceStatus = LOADING;
state.sequenceMightBeUnit = false;
},
fetchSequenceSuccess: (state, { payload }) => {
state.sequenceId = payload.sequenceId;
state.sequenceStatus = LOADED;
state.sequenceMightBeUnit = false;
},
fetchSequenceFailure: (state, { payload }) => {
state.sequenceId = payload.sequenceId;
state.sequenceStatus = FAILED;
state.sequenceMightBeUnit = payload.sequenceMightBeUnit || false;
},
},
});

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 }));
}
};
}