Skip to content

Commit

Permalink
fix: replaced the LMS endpoint for navigating the course unit page
Browse files Browse the repository at this point in the history
fix: [AXIMST-424] Course unit - Fixed network connection behavior (#138)

* fix: [AXIMST-424] fixed network connetcion behavior

* fix: added placeholder for unsuccessful loading for the page

* refactor: code refactoring
  • Loading branch information
PKulkoRaccoonGang committed Mar 10, 2024
1 parent 17b1360 commit e396b1c
Show file tree
Hide file tree
Showing 10 changed files with 31 additions and 391 deletions.
10 changes: 10 additions & 0 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import getPageHeadTitle from '../generic/utils';
import AlertMessage from '../generic/alert-message';
import ProcessingNotification from '../generic/processing-notification';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
import Loading from '../generic/Loading';
import AddComponent from './add-component/AddComponent';
import CourseXBlock from './course-xblock/CourseXBlock';
Expand All @@ -32,6 +33,7 @@ const CourseUnit = ({ courseId }) => {
sequenceId,
unitTitle,
isQueryPending,
sequenceStatus,
savingStatus,
isTitleEditFormOpen,
isErrorAlert,
Expand All @@ -57,6 +59,14 @@ const CourseUnit = ({ courseId }) => {
return <Loading />;
}

if (sequenceStatus === RequestStatus.FAILED) {
return (

Check warning on line 63 in src/course-unit/CourseUnit.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-unit/CourseUnit.jsx#L63

Added line #L63 was not covered by tests
<Container size="xl" className="course-unit px-4 mt-4">
<ConnectionErrorAlert />
</Container>
);
}

return (
<>
<Container size="xl" className="course-unit px-4">
Expand Down
4 changes: 4 additions & 0 deletions src/course-unit/__mocks__/courseSectionVertical.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ module.exports = {
},
},
],
course_sequence_ids: [
'block-v1:edx+876+2030+type@sequential+block@297321078a0f4c26a50d671ed87642a6',
'block-v1:edx+876+2030+type@sequential+block@4e91bdfefd8e4173a03d19c4d91e1936',
],
xblock_info: {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
display_name: 'Getting Started',
Expand Down
12 changes: 3 additions & 9 deletions src/course-unit/course-sequence/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,14 @@ import { useLayoutEffect, useRef, useState } from 'react';
import { useWindowSize } from '@openedx/paragon';

import { useModel } from '../../generic/model-store';
import {
getCourseSectionVertical,
getCourseUnit,
sequenceIdsSelector,
} from '../data/selectors';
import { getCourseSectionVertical, getSequenceIds } from '../data/selectors';

export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId) {
const sequenceIds = useSelector(sequenceIdsSelector);
export function useSequenceNavigationMetadata(courseId, currentSequenceId, currentUnitId) {
const { nextUrl, prevUrl } = useSelector(getCourseSectionVertical);
const sequence = useModel('sequences', currentSequenceId);
const { courseId } = useSelector(getCourseUnit);
const isFirstUnit = !prevUrl;
const isLastUnit = !nextUrl;

const sequenceIds = useSelector(getSequenceIds);
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
const unitIndex = sequence.unitIds.indexOf(currentUnitId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SequenceNavigationTabs from './SequenceNavigationTabs';

const SequenceNavigation = ({
intl,
courseId,
unitId,
sequenceId,
className,
Expand All @@ -28,7 +29,7 @@ const SequenceNavigation = ({
const sequenceStatus = useSelector(getSequenceStatus);
const {
isFirstUnit, isLastUnit, nextLink, previousLink,
} = useSequenceNavigationMetadata(sequenceId, unitId);
} = useSequenceNavigationMetadata(courseId, sequenceId, unitId);
const sequence = useModel('sequences', sequenceId);

const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
Expand Down Expand Up @@ -104,6 +105,7 @@ const SequenceNavigation = ({

SequenceNavigation.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
unitId: PropTypes.string,
className: PropTypes.string,
sequenceId: PropTypes.string,
Expand Down
51 changes: 1 addition & 50 deletions src/course-unit/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,13 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import { PUBLISH_TYPES } from '../constants';
import {
normalizeLearningSequencesData,
normalizeMetadata,
normalizeCourseHomeCourseMetadata,
appendBrowserTimezoneToUrl,
normalizeCourseSectionVerticalData,
} from './utils';
import { normalizeCourseSectionVerticalData } from './utils';

const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
const getLmsBaseUrl = () => getConfig().LMS_BASE_URL;

export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
export const getLearningSequencesOutlineApiUrl = (courseId) => `${getLmsBaseUrl()}/api/learning_sequences/v1/course_outline/${courseId}`;
export const getCourseMetadataApiUrl = (courseId) => `${getLmsBaseUrl()}/api/courseware/course/${courseId}`;
export const getCourseHomeCourseMetadataApiUrl = (courseId) => `${getLmsBaseUrl()}/api/course_home/course_metadata/${courseId}`;
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;

export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
Expand Down Expand Up @@ -65,45 +55,6 @@ export async function getCourseSectionVerticalData(unitId) {
return normalizeCourseSectionVerticalData(data);
}

/**
* Retrieves the outline of learning sequences for a specific course.
* @param {string} courseId - The ID of the course.
* @returns {Promise<Object>} A Promise that resolves to the normalized learning sequences outline data.
*/
export async function getLearningSequencesOutline(courseId) {
const { href } = new URL(getLearningSequencesOutlineApiUrl(courseId));
const { data } = await getAuthenticatedHttpClient().get(href, {});

return normalizeLearningSequencesData(data);
}

/**
* Retrieves metadata for a specific course.
* @param {string} courseId - The ID of the course.
* @returns {Promise<Object>} A Promise that resolves to the normalized course metadata.
*/
export async function getCourseMetadata(courseId) {
let courseMetadataApiUrl = getCourseMetadataApiUrl(courseId);
courseMetadataApiUrl = appendBrowserTimezoneToUrl(courseMetadataApiUrl);
const metadata = await getAuthenticatedHttpClient().get(courseMetadataApiUrl);

return normalizeMetadata(metadata);
}

/**
* Retrieves metadata for a course's home page.
* @param {string} courseId - The ID of the course.
* @param {string} rootSlug - The root slug for the course.
* @returns {Promise<Object>} A Promise that resolves to the normalized course home page metadata.
*/
export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
let courseHomeCourseMetadataApiUrl = getCourseHomeCourseMetadataApiUrl(courseId);
courseHomeCourseMetadataApiUrl = appendBrowserTimezoneToUrl(courseHomeCourseMetadataApiUrl);
const { data } = await getAuthenticatedHttpClient().get(courseHomeCourseMetadataApiUrl);

return normalizeCourseHomeCourseMetadata(data, rootSlug);
}

/**
* Creates a new course XBlock.
* @param {Object} options - The options for creating the XBlock.
Expand Down
29 changes: 4 additions & 25 deletions src/course-unit/data/selectors.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,9 @@
import { createSelector } from '@reduxjs/toolkit';

import { RequestStatus } from '../../data/constants';

export const getCourseUnitData = (state) => state.courseUnit.unit;
export const getCourseUnit = (state) => state.courseUnit;
export const getSavingStatus = (state) => state.courseUnit.savingStatus;
export const getLoadingStatus = (state) => state.courseUnit.loadingStatus;
export const getSequenceStatus = (state) => state.courseUnit.sequenceStatus;
export const getSequenceIds = (state) => state.courseUnit.courseSectionVertical.courseSequenceIds;
export const getCourseSectionVertical = (state) => state.courseUnit.courseSectionVertical;
export const getCourseUnitComponentTemplates = (state) => state.courseUnit.courseSectionVertical.componentTemplates;
export const getCourseSectionVerticalLoadingStatus = (state) => state
.courseUnit.loadingStatus.courseSectionVerticalLoadingStatus;
export const getCourseStatus = state => state.courseUnit.courseStatus;
export const getCoursewareMeta = state => state.models.coursewareMeta;
export const getSections = state => state.models.sections;
export const getCourseId = state => state.courseDetail.courseId;
export const getSequenceId = state => state.courseUnit.sequenceId;
export const getCourseVerticalChildren = state => state.courseUnit.courseVerticalChildren;
export const sequenceIdsSelector = createSelector(
[getCourseStatus, getCoursewareMeta, getSections, getCourseId],
(courseStatus, coursewareMeta, sections, courseId) => {
if (courseStatus !== RequestStatus.SUCCESSFUL) {
return [];
}

const sectionIds = coursewareMeta[courseId].sectionIds || [];
return sectionIds.flatMap(sectionId => sections[sectionId].sequenceIds);
},
);
export const getCourseId = (state) => state.courseDetail.courseId;
export const getSequenceId = (state) => state.courseUnit.sequenceId;
export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerticalChildren;
20 changes: 0 additions & 20 deletions src/course-unit/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,6 @@ const slice = createSlice({
state.sequenceStatus = RequestStatus.FAILED;
state.sequenceMightBeUnit = payload.sequenceMightBeUnit || false;
},
fetchCourseRequest: (state, { payload }) => {
state.courseId = payload.courseId;
state.courseStatus = RequestStatus.IN_PROGRESS;
},
fetchCourseSuccess: (state, { payload }) => {
state.courseId = payload.courseId;
state.courseStatus = RequestStatus.SUCCESSFUL;
},
fetchCourseFailure: (state, { payload }) => {
state.courseId = payload.courseId;
state.courseStatus = RequestStatus.FAILED;
},
fetchCourseDenied: (state, { payload }) => {
state.courseId = payload.courseId;
state.courseStatus = RequestStatus.DENIED;
},
fetchCourseSectionVerticalDataSuccess: (state, { payload }) => {
state.courseSectionVertical = payload;
},
Expand Down Expand Up @@ -122,10 +106,6 @@ export const {
fetchSequenceRequest,
fetchSequenceSuccess,
fetchSequenceFailure,
fetchCourseRequest,
fetchCourseSuccess,
fetchCourseFailure,
fetchCourseDenied,
fetchCourseSectionVerticalDataSuccess,
updateLoadingCourseSectionVerticalDataStatus,
changeEditTitleFormOpen,
Expand Down
94 changes: 1 addition & 93 deletions src/course-unit/data/thunk.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { camelCaseObject } from '@edx/frontend-platform';

import {
Expand All @@ -7,15 +6,10 @@ import {
} from '../../generic/processing-notification/data/slice';
import { RequestStatus } from '../../data/constants';
import { NOTIFICATION_MESSAGES } from '../../constants';
import {
addModel, updateModel, updateModels, updateModelsMap, addModelsMap,
} from '../../generic/model-store';
import { updateModel, updateModels } from '../../generic/model-store';
import {
getCourseUnitData,
editUnitDisplayName,
getCourseMetadata,
getLearningSequencesOutline,
getCourseHomeCourseMetadata,
getCourseSectionVerticalData,
createCourseXblock,
getCourseVerticalChildren,
Expand All @@ -30,10 +24,6 @@ import {
fetchSequenceRequest,
fetchSequenceFailure,
fetchSequenceSuccess,
fetchCourseRequest,
fetchCourseSuccess,
fetchCourseDenied,
fetchCourseFailure,
fetchCourseSectionVerticalDataSuccess,
updateLoadingCourseSectionVerticalDataStatus,
updateLoadingCourseXblockStatus,
Expand Down Expand Up @@ -146,88 +136,6 @@ export function editCourseUnitVisibilityAndData(itemId, type, isVisible) {
};
}

export function fetchCourse(courseId) {
return async (dispatch) => {
dispatch(fetchCourseRequest({ courseId }));
Promise.allSettled([
getCourseMetadata(courseId),
getLearningSequencesOutline(courseId),
getCourseHomeCourseMetadata(courseId, 'courseware'),
]).then(([
courseMetadataResult,
learningSequencesOutlineResult,
courseHomeMetadataResult]) => {
if (courseMetadataResult.status === 'fulfilled') {
dispatch(addModel({
modelType: 'coursewareMeta',
model: courseMetadataResult.value,
}));
}

if (courseHomeMetadataResult.status === 'fulfilled') {
dispatch(addModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeMetadataResult.value,
},
}));
}

if (learningSequencesOutlineResult.status === 'fulfilled') {
const { courses, sections } = 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,
}));
}

const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
const fetchedCourseHomeMetadata = courseHomeMetadataResult.status === 'fulfilled';
const fetchedOutline = learningSequencesOutlineResult.status === 'fulfilled';

// 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 (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 createNewCourseXBlock(body, callback, blockId) {
return async (dispatch) => {
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.IN_PROGRESS }));
Expand Down
Loading

0 comments on commit e396b1c

Please sign in to comment.