Files
frontend-app-learning/src/courseware/data/thunks.js
Javier Ontiveros 31b02d777f feat: disable completion item icons on flag (#1714)
* feat: base modifications to disable completion checks when flag enabled

* chore: started updating tests

* chore: udpated tests

* chore: added missing negative test

---------

Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
2025-06-04 15:33:12 -03:00

292 lines
9.5 KiB
JavaScript

import { logError, logInfo } from '@edx/frontend-platform/logging';
import { getCourseHomeCourseMetadata } from '../../course-home/data/api';
import {
addModel, addModelsMap, updateModel, updateModels, updateModelsMap,
} from '../../generic/model-store';
import {
getBlockCompletion,
getCourseDiscussionConfig,
getCourseMetadata,
getCourseOutline,
getCourseTopics,
getCoursewareOutlineSidebarToggles,
getLearningSequencesOutline,
getSequenceMetadata,
postIntegritySignature,
postSequencePosition,
} from './api';
import {
fetchCourseDenied,
fetchCourseFailure,
fetchCourseRequest,
fetchCourseSuccess,
fetchSequenceFailure,
fetchSequenceRequest,
fetchSequenceSuccess,
fetchCourseOutlineRequest,
fetchCourseOutlineSuccess,
fetchCourseOutlineFailure,
setCoursewareOutlineSidebarToggles,
updateCourseOutlineCompletion,
} from './slice';
export function fetchCourse(courseId) {
return async (dispatch) => {
dispatch(fetchCourseRequest({ courseId }));
Promise.allSettled([
getCourseMetadata(courseId),
getLearningSequencesOutline(courseId),
getCourseHomeCourseMetadata(courseId, 'courseware'),
getCoursewareOutlineSidebarToggles(courseId),
]).then(([
courseMetadataResult,
learningSequencesOutlineResult,
courseHomeMetadataResult,
coursewareOutlineSidebarTogglesResult]) => {
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
const fetchedCourseHomeMetadata = courseHomeMetadataResult.status === 'fulfilled';
const fetchedOutline = learningSequencesOutlineResult.status === 'fulfilled';
const fetchedCoursewareOutlineSidebarTogglesResult = coursewareOutlineSidebarTogglesResult.status === 'fulfilled';
if (fetchedMetadata) {
dispatch(addModel({
modelType: 'coursewareMeta',
model: courseMetadataResult.value,
}));
}
if (fetchedCourseHomeMetadata) {
dispatch(addModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeMetadataResult.value,
},
}));
}
if (fetchedOutline) {
const {
courses, sections, sequences,
} = learningSequencesOutlineResult.value;
// This updates the course with a sectionIds array from the Learning Sequence data.
dispatch(updateModelsMap({
modelType: 'coursewareMeta',
modelsMap: courses,
}));
dispatch(addModelsMap({
modelType: 'sections',
modelsMap: sections,
}));
// We update for sequences because the sequence metadata may have come back first.
dispatch(updateModelsMap({
modelType: 'sequences',
modelsMap: sequences,
}));
}
if (fetchedCoursewareOutlineSidebarTogglesResult) {
const {
enable_navigation_sidebar: enableNavigationSidebar,
always_open_auxiliary_sidebar: alwaysOpenAuxiliarySidebar,
enable_completion_tracking: enableCompletionTracking,
} = coursewareOutlineSidebarTogglesResult.value;
dispatch(setCoursewareOutlineSidebarToggles(
{ enableNavigationSidebar, alwaysOpenAuxiliarySidebar, enableCompletionTracking },
));
}
// Log errors for each request if needed. Outline failures may occur
// even if the course metadata request is successful
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(learningSequencesOutlineResult.reason);
} else {
logError(learningSequencesOutlineResult.reason);
}
}
if (!fetchedMetadata) {
logError(courseMetadataResult.reason);
}
if (!fetchedCourseHomeMetadata) {
logError(courseHomeMetadataResult.reason);
}
if (!fetchedCoursewareOutlineSidebarTogglesResult) {
logError(coursewareOutlineSidebarTogglesResult.reason);
}
if (fetchedMetadata && fetchedCourseHomeMetadata) {
if (courseHomeMetadataResult.value.courseAccess.hasAccess && fetchedOutline) {
// User has access
dispatch(fetchCourseSuccess({ courseId }));
return;
}
// User either doesn't have access or only has partial access
// (can't access course blocks)
dispatch(fetchCourseDenied({ courseId }));
return;
}
// Definitely an error happening
dispatch(fetchCourseFailure({ courseId }));
});
};
}
export function fetchSequence(sequenceId, isPreview) {
return async (dispatch) => {
dispatch(fetchSequenceRequest({ sequenceId }));
try {
const { sequence, units } = await getSequenceMetadata(sequenceId, { preview: isPreview ? '1' : '0' });
if (sequence.blockType !== 'sequential') {
// Some other block types (particularly 'chapter') can be returned
// by this API. We want to error in that case, since downstream
// courseware code is written to render Sequences of Units.
logError(
`Requested sequence '${sequenceId}' `
+ `has block type '${sequence.blockType}'; expected block type 'sequential'.`,
);
dispatch(fetchSequenceFailure({ sequenceId }));
} else {
dispatch(updateModel({
modelType: 'sequences',
model: sequence,
}));
dispatch(updateModels({
modelType: 'units',
models: units,
}));
dispatch(fetchSequenceSuccess({ sequenceId }));
}
} catch (error) {
// 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 sequenceMightBeUnit = error?.response?.status === 422;
if (!sequenceMightBeUnit) {
logError(error);
}
dispatch(fetchSequenceFailure({ sequenceId, sequenceMightBeUnit }));
}
};
}
export function checkBlockCompletion(courseId, sequenceId, unitId) {
return async (dispatch, getState) => {
const { models } = getState();
if (models.units[unitId]?.complete) {
return {}; // do nothing. Things don't get uncompleted after they are completed.
}
try {
const isComplete = await getBlockCompletion(courseId, sequenceId, unitId);
dispatch(updateModel({
modelType: 'units',
model: {
id: unitId,
complete: isComplete,
},
}));
dispatch(updateCourseOutlineCompletion({ sequenceId, unitId, isComplete }));
return isComplete;
} catch (error) {
logError(error);
}
return {};
};
}
export function saveSequencePosition(courseId, sequenceId, activeUnitIndex) {
return async (dispatch, getState) => {
const { models } = getState();
const initialActiveUnitIndex = models.sequences[sequenceId].activeUnitIndex;
// Optimistically update the position.
dispatch(updateModel({
modelType: 'sequences',
model: {
id: sequenceId,
activeUnitIndex,
},
}));
try {
await postSequencePosition(courseId, sequenceId, activeUnitIndex);
// Update again under the assumption that the above call succeeded, since it doesn't return a
// meaningful response.
dispatch(updateModel({
modelType: 'sequences',
model: {
id: sequenceId,
activeUnitIndex,
},
}));
} catch (error) {
logError(error);
dispatch(updateModel({
modelType: 'sequences',
model: {
id: sequenceId,
activeUnitIndex: initialActiveUnitIndex,
},
}));
}
};
}
export function saveIntegritySignature(courseId, isMasquerading) {
return async (dispatch) => {
try {
// If the request is made by a staff user masquerading as a specific learner,
// don't actually create a signature for them on the backend,
// only the modal dialog will be dismissed
if (!isMasquerading) {
await postIntegritySignature(courseId);
}
dispatch(updateModel({
modelType: 'coursewareMeta',
model: {
id: courseId,
userNeedsIntegritySignature: false,
},
}));
} catch (error) {
logError(error);
}
};
}
export function getCourseDiscussionTopics(courseId) {
return async (dispatch) => {
try {
const config = await getCourseDiscussionConfig(courseId);
// Only load topics for the openedx provider, the legacy provider uses
// the xblock
if (config.provider === 'openedx') {
const topics = await getCourseTopics(courseId);
dispatch(updateModels({
modelType: 'discussionTopics',
models: topics.filter(topic => topic.usageKey),
idField: 'usageKey',
}));
}
} catch (error) {
logError(error);
}
};
}
export function getCourseOutlineStructure(courseId) {
return async (dispatch) => {
dispatch(fetchCourseOutlineRequest());
try {
const courseOutline = await getCourseOutline(courseId);
dispatch(fetchCourseOutlineSuccess({ courseOutline }));
} catch (error) {
logError(error);
dispatch(fetchCourseOutlineFailure());
}
};
}