fix: stop stripping most inaccessible sequences from navigation

I had previously made a "fix" to strip all inaccessible sequences
from the learning sequence outline hierarachy, as a way to filter
out unreleased sequences. See commit d1f19a9.

But that was too big a hammer and stripped a lot of released-but-
inaccessible sequences too (e.g. prerequisites).

So now, we adopt a more nuanced approach and explicitly just filter
out sequences that are both inaccessible AND unreleased.

AA-1219
This commit is contained in:
Michael Terry
2022-03-25 14:13:33 -04:00
committed by Michael Terry
parent 0a70f9b64e
commit 7bb2266790
3 changed files with 45 additions and 23 deletions

View File

@@ -46,6 +46,7 @@ export function buildOutlineFromBlocks(courseBlocks) {
id: block.id,
title: block.display_name,
start: null,
effective_start: null,
sequence_ids: [...block.children],
};
} else if (block.type === 'sequential') {
@@ -54,6 +55,7 @@ export function buildOutlineFromBlocks(courseBlocks) {
title: block.display_name,
accessible: true,
start: null,
effective_start: null,
};
}
});

View File

@@ -10,11 +10,17 @@ export function normalizeLearningSequencesData(learningSequencesData) {
sequences: {},
};
const now = new Date();
function isReleased(block) {
// We check whether the backend marks this as accessible because staff users are granted access anyway.
// Note that sections don't have the `accessible` field and will just be checking `effective_start`.
return block.accessible || !block.effective_start || now >= Date.parse(block.effective_start);
}
// 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;
if (!isReleased(sequence)) {
return; // Don't let the learner see unreleased sequences
}
models.sequences[seqId] = {
@@ -26,39 +32,37 @@ export function normalizeLearningSequencesData(learningSequencesData) {
// 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 (section.sequence_ids.length > 0 && accessibleSequenceIds.length === 0) {
// Filter out any ignored sequences (e.g. unreleased sequences)
const availableSequenceIds = section.sequence_ids.filter(seqId => seqId in models.sequences);
// If we are unreleased and already stripped out all our children, just don't show us at all.
// (We check both release date and children because children will exist for an unreleased section even for staff,
// so we still want to show this section.)
if (!isReleased(section) && availableSequenceIds.length === 0) {
return;
}
models.sections[section.id] = {
id: section.id,
title: section.title,
sequenceIds: accessibleSequenceIds,
sequenceIds: availableSequenceIds,
courseId: learningSequencesData.course_key,
};
// Add back-references to this section for all child sequences.
accessibleSequenceIds.forEach(childSeqId => {
availableSequenceIds.forEach(childSeqId => {
models.sequences[childSeqId].sectionId = section.id;
});
});
// Course
const now = new Date();
models.courses[learningSequencesData.course_key] = {
id: learningSequencesData.course_key,
title: learningSequencesData.title,
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
// inaccessible because the end_date has passed.)
hasScheduledContent: Object.values(learningSequencesData.outline.sequences).some(
seq => !seq.accessible && now < Date.parse(seq.effective_start),
),
// Scan through all the sequences and look for ones that aren't released yet.
hasScheduledContent: Object.values(learningSequencesData.outline.sequences).some(seq => !isReleased(seq)),
};
return models;

View File

@@ -80,9 +80,9 @@ describe('Courseware Service', () => {
expect(response).toEqual(normalizedOutline);
});
it('skips inaccessible sequences', async () => {
it('skips unreleased sequences', async () => {
await provider.addInteraction({
state: `Outline exists with inaccessible sequences for course_id ${courseId}`,
state: `Outline exists with unreleased sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
@@ -97,18 +97,21 @@ describe('Courseware Service', () => {
sections: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially accessible',
title: 'Partially released',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
],
effective_start: null,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope',
title: 'Wholly inaccessible',
title: 'Wholly unreleased',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
],
effective_start: '9999-07-01T17:00:00Z',
},
],
sequences: {
@@ -116,17 +119,23 @@ describe('Courseware Service', () => {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
accessible: true,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
accessible: false,
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',
title: 'Unreleased',
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',
title: 'Still unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
@@ -149,10 +158,11 @@ describe('Courseware Service', () => {
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',
title: 'Partially released',
courseId: 'course-v1:edX+DemoX+Demo_Course',
sequenceIds: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
],
},
},
@@ -163,6 +173,12 @@ describe('Courseware Service', () => {
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`,
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
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@released?experience=legacy`,
},
},
};
const response = await getLearningSequencesOutline(courseId);