fix: skip inaccessible learning sequence data (#716)

When normalizing learning sequences, skip inaccessible sequences
and also skip sections with only inaccessible sequences.

This both imitates the legacy course block behavior and also
avoids a failure when merging course block data with LS data
when they disagree about which sequences exist.
This commit is contained in:
Michael Terry
2021-10-29 10:53:50 -04:00
committed by GitHub
parent 8a3722a723
commit d1f19a9dc4
3 changed files with 284 additions and 19 deletions

View File

@@ -98,12 +98,40 @@ export function normalizeLearningSequencesData(learningSequencesData) {
sequences: {},
};
// Sequences
Object.entries(learningSequencesData.outline.sequences).forEach(([seqId, sequence]) => {
if (!sequence.accessible) {
// Skipping inaccessible sequences replicates the behavior of the legacy course blocks API
return;
}
models.sequences[seqId] = {
id: seqId,
title: sequence.title,
};
});
// Sections
learningSequencesData.outline.sections.forEach(section => {
// Skipping sections with only inaccessible sequences replicates the behavior of the legacy course blocks API
const accessibleSequenceIds = section.sequence_ids.filter(seqId => seqId in models.sequences);
if (accessibleSequenceIds.length === 0) {
return;
}
models.sections[section.id] = {
id: section.id,
title: section.title,
sequenceIds: accessibleSequenceIds,
};
});
// Course
const now = new Date();
models.courses[learningSequencesData.course_key] = {
id: learningSequencesData.course_key,
title: learningSequencesData.title,
sectionIds: learningSequencesData.outline.sections.map(section => section.id),
sectionIds: Object.entries(models.sections).map(([sectionId]) => sectionId),
// Scan through all the sequences and look for ones that aren't accessible
// to us yet because the start date has not yet passed. (Some may be
@@ -113,23 +141,6 @@ export function normalizeLearningSequencesData(learningSequencesData) {
),
};
// Sections
learningSequencesData.outline.sections.forEach(section => {
models.sections[section.id] = {
id: section.id,
title: section.title,
sequenceIds: section.sequence_ids,
};
});
// Sequences
Object.entries(learningSequencesData.outline.sequences).forEach(([seqId, sequence]) => {
models.sequences[seqId] = {
id: seqId,
title: sequence.title,
};
});
return models;
}

View File

@@ -6,6 +6,7 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
getCourseBlocks,
getCourseMetadata,
getLearningSequencesOutline,
getSequenceMetadata,
postSequencePosition,
getBlockCompletion,
@@ -96,6 +97,130 @@ describe('Courseware Service', () => {
});
});
describe('When a request to get a learning sequence outline is made', () => {
it('returns a normalized outline', async () => {
await provider.addInteraction({
state: `Outline exists for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('block-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: {
sections: [],
sequences: {},
},
},
},
});
const normalizedOutline = {
courses: {
'block-v1:edX+DemoX+Demo_Course': {
id: 'block-v1:edX+DemoX+Demo_Course',
title: 'Demo Course',
sectionIds: [],
hasScheduledContent: false,
},
},
sections: {},
sequences: {},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
});
it('skips inaccessible sequences', async () => {
await provider.addInteraction({
state: `Outline exists with inaccessible sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('block-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: like({
sections: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially accessible',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
],
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope',
title: 'Wholly inaccessible',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
],
},
],
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
accessible: true,
effective_start: '2019-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
title: 'Cannot access',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
title: 'Still cannot access',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
},
}),
},
},
});
const normalizedOutline = {
courses: {
'block-v1:edX+DemoX+Demo_Course': {
id: 'block-v1:edX+DemoX+Demo_Course',
title: 'Demo Course',
sectionIds: [
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
],
hasScheduledContent: true,
},
},
sections: {
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial': {
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially accessible',
sequenceIds: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
],
},
},
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
},
},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
});
});
describe('When a request to get course metadata is made', () => {
it('returns normalized course metadata', async () => {
await provider.addInteraction({

View File

@@ -623,6 +623,135 @@
"headers": {
}
}
},
{
"description": "a request to get a learning sequence outline",
"providerState": "Learning sequence outline exists for course_id course-v1:edX+DemoX+Demo_Course",
"request": {
"method": "GET",
"path": "/api/learning_sequences/v1/course_outline/course-v1:edX+DemoX+Demo_Course"
},
"response": {
"status": 200,
"headers": {
},
"body": {
"course_key": "block-v1:edX+DemoX+Demo_Course",
"title": "Demo Course",
"outline": {
"sections": [
],
"sequences": [
]
}
},
"matchingRules": {
"$.body.course_key": {
"match": "type"
},
"$.body.title": {
"match": "type"
}
}
}
},
{
"description": "a request to get an outline",
"providerState": "Outline exists for course_id course-v1:edX+DemoX+Demo_Course",
"request": {
"method": "GET",
"path": "/api/learning_sequences/v1/course_outline/course-v1:edX+DemoX+Demo_Course"
},
"response": {
"status": 200,
"headers": {
},
"body": {
"course_key": "block-v1:edX+DemoX+Demo_Course",
"title": "Demo Course",
"outline": {
"sections": [
],
"sequences": {
}
}
},
"matchingRules": {
"$.body.course_key": {
"match": "type"
},
"$.body.title": {
"match": "type"
}
}
}
},
{
"description": "a request to get an outline",
"providerState": "Outline exists with inaccessible sequences for course_id course-v1:edX+DemoX+Demo_Course",
"request": {
"method": "GET",
"path": "/api/learning_sequences/v1/course_outline/course-v1:edX+DemoX+Demo_Course"
},
"response": {
"status": 200,
"headers": {
},
"body": {
"course_key": "block-v1:edX+DemoX+Demo_Course",
"title": "Demo Course",
"outline": {
"sections": [
{
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial",
"title": "Partially accessible",
"sequence_ids": [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible",
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1"
]
},
{
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope",
"title": "Wholly inaccessible",
"sequence_ids": [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2"
]
}
],
"sequences": {
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible": {
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible",
"title": "Can access",
"accessible": true
},
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1": {
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1",
"title": "Cannot access",
"accessible": false
},
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2": {
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2",
"title": "Still cannot access",
"accessible": false
}
}
}
},
"matchingRules": {
"$.body.course_key": {
"match": "type"
},
"$.body.title": {
"match": "type"
},
"$.body.outline": {
"match": "type"
}
}
}
}
],
"metadata": {
@@ -630,4 +759,4 @@
"version": "2.0.0"
}
}
}
}