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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
1
src/courseware/course/sequence/hidden-after-due/index.js
Normal file
1
src/courseware/course/sequence/hidden-after-due/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HiddenAfterDue';
|
||||
23
src/courseware/course/sequence/hidden-after-due/messages.js
Normal file
23
src/courseware/course/sequence/hidden-after-due/messages.js
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user