Skip to content

Commit

Permalink
fix: re-enable access error redirects for course home (openedx#570)
Browse files Browse the repository at this point in the history
These redirects are already in place for the courseware, and will
redirect to the outline page, or the dashboard, or wherever, based
on the type of access error (unenrolled, access expired, survey
needed, etc).

This commit stops the course home tabs from paying attention to the
401 error messages coming from the backend - course_access in the
metadata API handles that now.

This commit also changes our useModel hook to more gracefully handle
not being able to find the model - it is a valid case we want to
allow (still will cause problems if you actually try to use the data,
but will at least provide an object you can inspect).
  • Loading branch information
mikix authored Jul 30, 2021
1 parent 8c41e18 commit b4bedfe
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 71 deletions.
12 changes: 8 additions & 4 deletions src/course-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,12 @@ export async function getDatesTabData(courseId) {
const { httpErrorStatus } = error && error.customAttributes;
if (httpErrorStatus === 404) {
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`);
return {};
}
// 401 can be returned for unauthenticated users or users who are not enrolled
if (httpErrorStatus === 401) {
global.location.replace(`${getConfig().BASE_URL}/course/${courseId}/home`);
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
// courseAccess in the metadata call, so just ignore this status for now.
return {};
}
throw error;
}
Expand Down Expand Up @@ -266,10 +268,12 @@ export async function getProgressTabData(courseId, targetUserId) {
const { httpErrorStatus } = error && error.customAttributes;
if (httpErrorStatus === 404) {
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/progress`);
return {};
}
// 401 can be returned for unauthenticated users or users who are not enrolled
if (httpErrorStatus === 401) {
global.location.replace(`${getConfig().BASE_URL}/course/${courseId}/home`);
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
// courseAccess in the metadata call, so just ignore this status for now.
return {};
}
throw error;
}
Expand Down
7 changes: 3 additions & 4 deletions src/course-home/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../generic/model-store';

import {
// fetchTabDenied,
fetchTabDenied,
fetchTabFailure,
fetchTabRequest,
fetchTabSuccess,
Expand Down Expand Up @@ -63,10 +63,9 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) {
}

// Disable the access-denied path for now - it caused a regression
/* if (fetchedCourseHomeCourseMetadata && !courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
if (fetchedCourseHomeCourseMetadata && !courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
dispatch(fetchTabDenied({ courseId }));
} else */
if (fetchedCourseHomeCourseMetadata && fetchedTabData) {
} else if (fetchedCourseHomeCourseMetadata && fetchedTabData) {
dispatch(fetchTabSuccess({ courseId, targetUserId }));
} else {
dispatch(fetchTabFailure({ courseId }));
Expand Down
12 changes: 11 additions & 1 deletion src/course-home/dates-tab/DatesTab.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ describe('DatesTab', () => {
});
});

describe.skip('when receiving an access denied error', () => {
describe('when receiving an access denied error', () => {
// These tests could go into any particular tab, as they all go through the same flow. But dates tab works.

async function renderDenied(errorCode) {
Expand Down Expand Up @@ -338,5 +338,15 @@ describe('DatesTab', () => {
await renderDenied('course_not_started');
expect(global.location.href).toEqual('http://localhost/redirect/dashboard?notlive=2/5/2013'); // date from factory
});

it('redirects to the home page when unauthenticated', async () => {
await renderDenied('authentication_required');
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
});

it('redirects to the home page when unenrolled', async () => {
await renderDenied('enrollment_required');
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
});
});
});
4 changes: 2 additions & 2 deletions src/generic/model-store/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { useSelector, shallowEqual } from 'react-redux';

export function useModel(type, id) {
return useSelector(
state => (state.models[type] !== undefined ? state.models[type][id] : undefined),
state => (state.models[type] !== undefined ? state.models[type][id] : {}),
shallowEqual,
);
}

export function useModels(type, ids) {
return useSelector(
state => ids.map(
id => (state.models[type] !== undefined ? state.models[type][id] : undefined),
id => (state.models[type] !== undefined ? state.models[type][id] : {}),
),
shallowEqual,
);
Expand Down
3 changes: 0 additions & 3 deletions src/shared/access-denied-redirect/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
/* eslint-disable import/prefer-default-export */
import { getLocale } from '@edx/frontend-platform/i18n';
import { Redirect } from 'react-router';
import { useModel } from '../../generic/model-store';

// This component inspects an access denied error and redirects to a /redirect/... path, which then renders a nice
// little message while the browser loads the next page.
// This function inspects an access denied error and provides a redirect url (looks like a /redirect/... path),
// which then renders a nice little message while the browser loads the next page.
// This is basically a frontend version of check_course_access_with_redirect in the backend.

function AccessDeniedRedirect(props) {
const {
courseId,
metadataModel,
unitId,
} = props;

const {
courseAccess,
start,
} = useModel(metadataModel, courseId);

let url = `/redirect/course-home/${courseId}`;
export function getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess, start, unitId) {
let url = null;
switch (courseAccess.errorCode) {
case 'audit_expired':
url = `/redirect/dashboard?access_response_error=${courseAccess.additionalContextUserMessage}`;
Expand Down Expand Up @@ -48,20 +33,9 @@ function AccessDeniedRedirect(props) {
case 'authentication_required':
case 'enrollment_required':
default:
if (activeTabSlug !== 'outline') {
url = `/redirect/course-home/${courseId}`;
}
}
return (
<Redirect to={url} />
);
return url;
}

AccessDeniedRedirect.defaultProps = {
unitId: null,
};

AccessDeniedRedirect.propTypes = {
courseId: PropTypes.string.isRequired,
metadataModel: PropTypes.string.isRequired,
unitId: PropTypes.string,
};

export default AccessDeniedRedirect;
51 changes: 29 additions & 22 deletions src/tab-page/TabPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,37 @@ import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useDispatch, useSelector } from 'react-redux';
import { Redirect } from 'react-router';

import { Toast } from '@edx/paragon';
import { Header } from '../course-header';
import AccessDeniedRedirect from '../shared/access-denied-redirect';
import { getAccessDeniedRedirectUrl } from '../shared/access';
import PageLoading from '../generic/PageLoading';
import { useModel } from '../generic/model-store';

import genericMessages from '../generic/messages';
import messages from './messages';
import LoadedTabPage from './LoadedTabPage';
import { setCallToActionToast } from '../course-home/data/slice';

function TabPage({
intl,
courseId,
courseStatus,
metadataModel,
unitId,
...passthroughProps
}) {
function TabPage({ intl, ...props }) {
const {
activeTabSlug,
courseId,
courseStatus,
metadataModel,
unitId,
} = props;
const {
toastBodyLink,
toastBodyText,
toastHeader,
} = useSelector(state => state.courseHome);
const dispatch = useDispatch();
const {
courseAccess,
start,
} = useModel(metadataModel, courseId);

if (courseStatus === 'loading') {
return (
Expand All @@ -39,7 +45,16 @@ function TabPage({
);
}

if (courseStatus === 'loaded') {
if (courseStatus === 'denied') {
const redirectUrl = getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess, start, unitId);
if (redirectUrl) {
return (<Redirect to={redirectUrl} />);
}
}

// Either a success state or a denied state that wasn't redirected above (some tabs handle denied states themselves,
// like the outline tab handling unenrolled learners)
if (courseStatus === 'loaded' || courseStatus === 'denied') {
return (
<>
<Toast
Expand All @@ -53,21 +68,11 @@ function TabPage({
>
{toastHeader}
</Toast>
<LoadedTabPage courseId={courseId} metadataModel={metadataModel} unitId={unitId} {...passthroughProps} />
<LoadedTabPage {...props} />
</>
);
}

if (courseStatus === 'denied') {
return (
<AccessDeniedRedirect
courseId={courseId}
metadataModel={metadataModel}
unitId={unitId}
/>
);
}

// courseStatus 'failed' and any other unexpected course status.
return (
<>
Expand All @@ -80,12 +85,14 @@ function TabPage({
}

TabPage.defaultProps = {
courseId: null,
unitId: null,
};

TabPage.propTypes = {
activeTabSlug: PropTypes.string.isRequired,
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
courseId: PropTypes.string,
courseStatus: PropTypes.string.isRequired,
metadataModel: PropTypes.string.isRequired,
unitId: PropTypes.string,
Expand Down

0 comments on commit b4bedfe

Please sign in to comment.