feat: notify the user if a sequence is hidden because of due date (#636)

Normally, these sequences are skipped. But if the user manually
goes to the section, they should be notified why they can't access
it. That can easily happen if they bookmarked the page or something.

AA-1000
This commit is contained in:
Michael Terry
2021-09-10 11:13:48 -04:00
committed by GitHub
parent 73302d72cb
commit 90d6ea8137
9 changed files with 119 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ import { useModel } from '../../../generic/model-store';
import CourseLicense from '../course-license';
import messages from './messages';
import HiddenAfterDue from './hidden-after-due';
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
import SequenceContent from './SequenceContent';
import NotificationTray from '../NotificationTray';
@@ -144,6 +145,12 @@ function Sequence({
);
}
if (sequenceStatus === 'loaded' && sequence.isHiddenAfterDue) {
// Shouldn't even be here - these sequences are normally stripped out of the navigation.
// But we are here, so render a notice instead of the normal content.
return <HiddenAfterDue courseId={courseId} />;
}
/*
TODO: When the micro-frontend supports viewing special exams without redirecting to the legacy
experience, we can remove this whole conditional. For now, though, we show the spinner here

View File

@@ -81,6 +81,37 @@ describe('Sequence', () => {
expect(screen.queryByText('Loading locked content messaging...')).not.toBeInTheDocument();
});
it('renders correctly for hidden after due content', async () => {
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', children: [unitBlocks.map(block => block.id)] },
{ courseId: courseMetadata.id },
)];
const sequenceMetadata = [Factory.build(
'sequenceMetadata',
{ is_hidden_after_due: true },
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore(
{
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata,
}, false,
);
render(
<Sequence {...mockData} {...{ sequenceId: sequenceBlocks[0].id }} />,
{ store: testStore },
);
await waitFor(() => {
expect(screen.queryByText('The due date for this assignment has passed.')).toBeInTheDocument();
});
expect(screen.getByRole('link', { name: 'progress page' }))
.toHaveAttribute('href', 'http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress');
// No normal content or navigation should be rendered. Just the above alert.
expect(screen.queryAllByRole('button').length).toEqual(0);
});
it('renders correctly for exam content', async () => {
// Exams should NOT render in the Sequence. They should permanently show a spinner until the
// application redirects away from the page. Note that this component is not responsible for

View File

@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { useModel } from '../../../../generic/model-store';
import messages from './messages';
function HiddenAfterDue({ courseId, intl }) {
const { tabs } = useModel('coursewareMeta', courseId);
const progressTab = tabs.find(tab => tab.slug === 'progress');
const progressLink = progressTab && progressTab.url && (
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={progressTab.url}
className="text-reset"
>
{intl.formatMessage(messages.progressPage)}
</Hyperlink>
);
return (
<Alert variant="info" icon={Info}>
<h3>{intl.formatMessage(messages.header)}</h3>
<p>
{intl.formatMessage(messages.description)}
{progressLink && (
<>
<br />
<FormattedMessage
id="learn.hiddenAfterDue.gradeAvailable"
defaultMessage="If you have completed this assignment, your grade is available on the {progressPage}."
values={{
progressPage: progressLink,
}}
/>
</>
)}
</p>
</Alert>
);
}
HiddenAfterDue.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(HiddenAfterDue);

View File

@@ -0,0 +1 @@
export { default } from './HiddenAfterDue';

View File

@@ -0,0 +1,23 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
header: {
id: 'learn.hiddenAfterDue.header',
defaultMessage: 'The due date for this assignment has passed.',
},
description: {
id: 'learn.hiddenAfterDue.description',
defaultMessage: 'Because the due date has passed, this assignment is no longer available.',
},
gradeAvailable: {
id: 'learn.hiddenAfterDue.gradeAvailable',
defaultMessage: 'If you have completed this assignment, your grade is available on the {progressPage}.',
},
progressPage: {
id: 'learn.hiddenAfterDue.progressPage',
defaultMessage: 'progress page',
description: 'This is the text for the link embedded in learn.hiddenAfterDue.gradeAvailable',
},
});
export default messages;

View File

@@ -58,6 +58,7 @@ Factory.define('sequenceMetadata')
save_position: true,
prev_url: null,
is_time_limited: false,
is_hidden_after_due: false,
show_completion: true,
banner_text: null,
format: 'Homework',

View File

@@ -253,6 +253,7 @@ function normalizeSequenceMetadata(sequence) {
gatedContent: camelCaseObject(sequence.gated_content),
isTimeLimited: sequence.is_time_limited,
isProctored: sequence.is_proctored,
isHiddenAfterDue: sequence.is_hidden_after_due,
// Position comes back from the server 1-indexed. Adjust here.
activeUnitIndex: sequence.position ? sequence.position - 1 : 0,
saveUnitPosition: sequence.save_position,

View File

@@ -331,6 +331,7 @@ describe('Courseware Service', () => {
item_id: string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'),
is_time_limited: boolean(false),
is_proctored: boolean(false),
is_hidden_after_due: boolean(false),
position: null,
tag: boolean('sequential'),
banner_text: null,
@@ -367,6 +368,7 @@ describe('Courseware Service', () => {
},
isTimeLimited: false,
isProctored: false,
isHiddenAfterDue: false,
activeUnitIndex: 0,
saveUnitPosition: false,
showCompletion: false,

View File

@@ -489,6 +489,7 @@
"item_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
"is_time_limited": false,
"is_proctored": false,
"is_hidden_after_due": true,
"position": null,
"tag": "sequential",
"banner_text": null,