fix: handle course access errors in Course Home side of things too (#558)
The courseware was properly reading the access errors and redirecting the user as appropriate (like to the dashboard or whatever). This requires a backend change to push the error along.
This commit is contained in:
@@ -2,8 +2,6 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
import { Redirect } from 'react-router';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { defaultMemoize as memoize } from 'reselect';
|
||||
|
||||
@@ -245,42 +243,6 @@ class CoursewareContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderDenied() {
|
||||
const {
|
||||
course,
|
||||
courseId,
|
||||
match: {
|
||||
params: {
|
||||
unitId: routeUnitId,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
let url = `/redirect/course-home/${courseId}`;
|
||||
switch (course.canLoadCourseware.errorCode) {
|
||||
case 'audit_expired':
|
||||
url = `/redirect/dashboard?access_response_error=${course.canLoadCourseware.additionalContextUserMessage}`;
|
||||
break;
|
||||
case 'course_not_started':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const startDate = (new Intl.DateTimeFormat(getLocale())).format(new Date(course.start));
|
||||
url = `/redirect/dashboard?notlive=${startDate}`;
|
||||
break;
|
||||
case 'survey_required': // TODO: Redirect to the course survey
|
||||
case 'unfulfilled_milestones':
|
||||
url = '/redirect/dashboard';
|
||||
break;
|
||||
case 'microfrontend_disabled':
|
||||
url = `/redirect/courseware/${courseId}/unit/${routeUnitId}`;
|
||||
break;
|
||||
case 'authentication_required':
|
||||
case 'enrollment_required':
|
||||
default:
|
||||
}
|
||||
return (
|
||||
<Redirect to={url} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
courseStatus,
|
||||
@@ -293,10 +255,6 @@ class CoursewareContainer extends Component {
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (courseStatus === 'denied') {
|
||||
return this.renderDenied();
|
||||
}
|
||||
|
||||
return (
|
||||
<TabPage
|
||||
activeTabSlug="courseware"
|
||||
@@ -338,10 +296,9 @@ const sectionShape = PropTypes.shape({
|
||||
});
|
||||
|
||||
const courseShape = PropTypes.shape({
|
||||
canLoadCourseware: PropTypes.shape({
|
||||
errorCode: PropTypes.string,
|
||||
additionalContextUserMessage: PropTypes.string,
|
||||
}).isRequired,
|
||||
celebrations: PropTypes.shape({
|
||||
firstSection: PropTypes.bool,
|
||||
}),
|
||||
});
|
||||
|
||||
CoursewareContainer.propTypes = {
|
||||
|
||||
@@ -427,31 +427,46 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when receiving a can_load_courseware error_code', () => {
|
||||
describe('when receiving a course_access error_code', () => {
|
||||
function setUpWithDeniedStatus(errorCode) {
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
can_load_courseware: {
|
||||
course_access: {
|
||||
has_access: false,
|
||||
error_code: errorCode,
|
||||
additional_context_user_message: 'uhoh oh no', // only used by audit_expired
|
||||
},
|
||||
});
|
||||
const courseId = courseMetadata.id;
|
||||
const { courseBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name);
|
||||
|
||||
const { courseBlocks, sequenceBlocks, unitBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name);
|
||||
setUpMockRequests({ courseBlocks, courseMetadata });
|
||||
history.push(`/course/${courseId}`);
|
||||
return courseMetadata;
|
||||
history.push(`/course/${courseId}/${sequenceBlocks[0].id}/${unitBlocks[0].id}`);
|
||||
return { courseMetadata, unitBlocks };
|
||||
}
|
||||
|
||||
it('should go to course home for an enrollment_required error code', async () => {
|
||||
const courseMetadata = setUpWithDeniedStatus('enrollment_required');
|
||||
const { courseMetadata } = setUpWithDeniedStatus('enrollment_required');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
||||
});
|
||||
|
||||
it('should go to course survey for a survey_required error code', async () => {
|
||||
const { courseMetadata } = setUpWithDeniedStatus('survey_required');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/survey/${courseMetadata.id}`);
|
||||
});
|
||||
|
||||
it('should go to legacy courseware for a microfrontend_disabled error code', async () => {
|
||||
const { courseMetadata, unitBlocks } = setUpWithDeniedStatus('microfrontend_disabled');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/courseware/${courseMetadata.id}/unit/${unitBlocks[0].id}`);
|
||||
});
|
||||
|
||||
it('should go to course home for an authentication_required error code', async () => {
|
||||
const courseMetadata = setUpWithDeniedStatus('authentication_required');
|
||||
const { courseMetadata } = setUpWithDeniedStatus('authentication_required');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
||||
|
||||
@@ -31,6 +31,12 @@ export default () => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/course/`);
|
||||
}}
|
||||
/>
|
||||
<PageRoute
|
||||
path={`${path}/survey/:courseId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
|
||||
}}
|
||||
/>
|
||||
<PageRoute
|
||||
path={`${path}/dashboard`}
|
||||
render={({ location }) => {
|
||||
|
||||
@@ -34,7 +34,7 @@ Factory.define('courseMetadata')
|
||||
},
|
||||
show_calculator: false,
|
||||
license: 'all-rights-reserved',
|
||||
can_load_courseware: {
|
||||
course_access: {
|
||||
has_access: true,
|
||||
user_fragment: null,
|
||||
developer_message: null,
|
||||
|
||||
@@ -68,7 +68,7 @@ Factory.define('sequenceMetadata')
|
||||
*/
|
||||
export default function buildSimpleCourseAndSequenceMetadata(options = {}) {
|
||||
const courseMetadata = options.courseMetadata || Factory.build('courseMetadata', {
|
||||
can_load_courseware: {
|
||||
course_access: {
|
||||
has_access: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,7 +160,7 @@ function normalizeMetadata(metadata) {
|
||||
start: data.start,
|
||||
enrollmentMode: data.enrollment.mode,
|
||||
isEnrolled: data.enrollment.is_active,
|
||||
canLoadCourseware: camelCaseObject(data.can_load_courseware),
|
||||
courseAccess: camelCaseObject(data.course_access),
|
||||
canViewLegacyCourseware: data.can_view_legacy_courseware,
|
||||
originalUserIsStaff: data.original_user_is_staff,
|
||||
isStaff: data.is_staff,
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
it('Should fetch, normalize, and save metadata, but with denied status', async () => {
|
||||
const forbiddenCourseMetadata = Factory.build('courseMetadata', {
|
||||
can_load_courseware: {
|
||||
course_access: {
|
||||
has_access: false,
|
||||
},
|
||||
});
|
||||
@@ -89,7 +89,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.courseStatus).toEqual('denied');
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
expect(state.models.coursewareMeta[forbiddenCourseMetadata.id].canLoadCourseware).not.toBeUndefined();
|
||||
expect(state.models.coursewareMeta[forbiddenCourseMetadata.id].courseAccess).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should fetch, normalize, and save metadata', async () => {
|
||||
@@ -107,7 +107,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.sequenceId).toEqual(null);
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
expect(state.models.coursewareMeta[courseId].canLoadCourseware).not.toBeUndefined();
|
||||
expect(state.models.coursewareMeta[courseId].courseAccess).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should fetch, normalize, and save metadata; filtering has no effect', async () => {
|
||||
@@ -127,7 +127,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.sequenceId).toEqual(null);
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
expect(state.models.coursewareMeta[courseId].canLoadCourseware).not.toBeUndefined();
|
||||
expect(state.models.coursewareMeta[courseId].courseAccess).not.toBeUndefined();
|
||||
expect(state.models.sequences.length === 1);
|
||||
Object.values(state.models.sections).forEach(section => expect(section.sequenceIds.length === 1));
|
||||
});
|
||||
@@ -149,7 +149,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.sequenceId).toEqual(null);
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
expect(state.models.coursewareMeta[courseId].canLoadCourseware).not.toBeUndefined();
|
||||
expect(state.models.coursewareMeta[courseId].courseAccess).not.toBeUndefined();
|
||||
expect(state.models.sequences === null);
|
||||
Object.values(state.models.sections).forEach(section => expect(section.sequenceIds.length === 0));
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ export function fetchCourse(courseId) {
|
||||
}
|
||||
|
||||
if (fetchedMetadata) {
|
||||
if (courseMetadataResult.value.canLoadCourseware.hasAccess && fetchedBlocks) {
|
||||
if (courseMetadataResult.value.courseAccess.hasAccess && fetchedBlocks) {
|
||||
// User has access
|
||||
dispatch(fetchCourseSuccess({ courseId }));
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user