feat: add honor code component (#465)
This component blocks access to graded units when the user is required to sign the integrity agreement for the course. Once signed, it will not appear for the course again.
This commit is contained in:
@@ -21,6 +21,7 @@ import { fetchCourse } from '../../data/thunks';
|
||||
/** [MM-P2P] Experiment */
|
||||
import { MMP2PLockPaywall } from '../../../experiments/mm-p2p';
|
||||
|
||||
const HonorCode = React.lazy(() => import('./honor-code'));
|
||||
const LockPaywall = React.lazy(() => import('./lock-paywall'));
|
||||
|
||||
/**
|
||||
@@ -93,6 +94,7 @@ function Unit({
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
contentTypeGatingEnabled,
|
||||
userNeedsIntegritySignature,
|
||||
} = course;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -154,8 +156,19 @@ function Unit({
|
||||
{ mmp2p.meta.showLock && (
|
||||
<MMP2PLockPaywall options={mmp2p} />
|
||||
)}
|
||||
{!mmp2p.meta.blockContent && unit.graded && userNeedsIntegritySignature && (
|
||||
<Suspense
|
||||
fallback={(
|
||||
<PageLoading
|
||||
srMessage={intl.formatMessage(messages['learn.loading.honor.code'])}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<HonorCode courseId={courseId} />
|
||||
</Suspense>
|
||||
)}
|
||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
||||
{!mmp2p.meta.blockContent && !hasLoaded && (
|
||||
{!mmp2p.meta.blockContent && !userNeedsIntegritySignature && !hasLoaded && (
|
||||
<PageLoading
|
||||
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
|
||||
/>
|
||||
@@ -186,7 +199,7 @@ function Unit({
|
||||
/>
|
||||
)}
|
||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
||||
{ !mmp2p.meta.blockContent && (
|
||||
{ !mmp2p.meta.blockContent && !userNeedsIntegritySignature && (
|
||||
<div className="unit-iframe-wrapper">
|
||||
<iframe
|
||||
id="unit-iframe"
|
||||
|
||||
@@ -13,11 +13,16 @@ describe('Unit', () => {
|
||||
);
|
||||
const unitBlocks = [Factory.build(
|
||||
'block',
|
||||
{ type: 'problem' },
|
||||
{ type: 'problem', graded: 'true' },
|
||||
{ courseId: courseMetadata.id },
|
||||
), Factory.build(
|
||||
'block',
|
||||
{ type: 'vertical', contains_content_type_gated_content: true, bookmarked: true },
|
||||
{
|
||||
type: 'vertical',
|
||||
contains_content_type_gated_content: true,
|
||||
bookmarked: true,
|
||||
graded: true,
|
||||
},
|
||||
{ courseId: courseMetadata.id },
|
||||
)];
|
||||
const [unit, unitThatContainsGatedContent] = unitBlocks;
|
||||
@@ -49,6 +54,24 @@ describe('Unit', () => {
|
||||
expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays HonorCode when userNeedsIntegritySignature is true', async () => {
|
||||
const signatureMetadata = Factory.build(
|
||||
'courseMetadata',
|
||||
{ user_needs_integrity_signature: true },
|
||||
);
|
||||
const signatureStore = await initializeTestStore(
|
||||
{ courseMetadata: signatureMetadata, unitBlocks },
|
||||
false,
|
||||
);
|
||||
const signatureData = {
|
||||
id: unit.id,
|
||||
courseId: signatureMetadata.id,
|
||||
format: 'Homework',
|
||||
};
|
||||
render(<Unit {...signatureData} />, { store: signatureStore });
|
||||
expect(screen.getByText('Loading honor code messaging...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles receiving MessageEvent', async () => {
|
||||
render(<Unit {...mockData} />);
|
||||
loadUnit();
|
||||
|
||||
58
src/courseware/course/sequence/honor-code/HonorCode.jsx
Normal file
58
src/courseware/course/sequence/honor-code/HonorCode.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getConfig, history } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Alert, Button } from '@edx/paragon';
|
||||
|
||||
import { saveIntegritySignature } from '../../../data';
|
||||
import messages from './messages';
|
||||
|
||||
function HonorCode({ intl, courseId }) {
|
||||
const dispatch = useDispatch();
|
||||
const siteName = getConfig().SITE_NAME;
|
||||
const honorCodeUrl = `${process.env.TERMS_OF_SERVICE_URL}#honor-code`;
|
||||
|
||||
const handleCancel = () => history.push(`/course/${courseId}/home`);
|
||||
|
||||
const handleAgree = () => {
|
||||
dispatch(saveIntegritySignature(courseId));
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert variant="light" aria-live="off">
|
||||
<h4 aria-level="3">
|
||||
{siteName}{' '}
|
||||
{intl.formatMessage(messages['learn.honorCode.name'])}
|
||||
</h4>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="learn.honorCode.content"
|
||||
defaultMessage="Honesty and academic integrity are important to {siteName} and the institutions providing courses and programs on the {siteName} site. By clicking “I agree” below, I confirm that I have read, understand, and will abide by the {link} for the {siteName} Site."
|
||||
values={{
|
||||
siteName,
|
||||
link: <a href={honorCodeUrl}>{intl.formatMessage(messages['learn.honorCode.name'])}</a>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<ActionRow>
|
||||
<ActionRow.Spacer />
|
||||
<Button variant="tertiary" onClick={handleCancel}>
|
||||
{intl.formatMessage(messages['learn.honorCode.cancel'])}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleAgree}>
|
||||
{intl.formatMessage(messages['learn.honorCode.agree'])}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
HonorCode.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(HonorCode);
|
||||
33
src/courseware/course/sequence/honor-code/HonorCode.test.jsx
Normal file
33
src/courseware/course/sequence/honor-code/HonorCode.test.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
import {
|
||||
fireEvent, initializeTestStore, render, screen,
|
||||
} from '../../../../setupTest';
|
||||
import HonorCode from './HonorCode';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform'),
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Honor Code', () => {
|
||||
let store;
|
||||
const mockData = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
store = await initializeTestStore();
|
||||
const { courseware } = store.getState();
|
||||
mockData.courseId = courseware.courseId;
|
||||
});
|
||||
|
||||
it('cancel button links to course home ', () => {
|
||||
render(<HonorCode {...mockData} />);
|
||||
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/home`);
|
||||
});
|
||||
});
|
||||
1
src/courseware/course/sequence/honor-code/index.js
Normal file
1
src/courseware/course/sequence/honor-code/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HonorCode';
|
||||
21
src/courseware/course/sequence/honor-code/messages.js
Normal file
21
src/courseware/course/sequence/honor-code/messages.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'learn.honorCode.name': {
|
||||
id: 'learn.honorCode.name',
|
||||
defaultMessage: 'Honor Code',
|
||||
description: 'Honor code name.',
|
||||
},
|
||||
'learn.honorCode.cancel': {
|
||||
id: 'learn.honorCode.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
description: '"Cancel" button.',
|
||||
},
|
||||
'learn.honorCode.agree': {
|
||||
id: 'learn.honorCode.agree',
|
||||
defaultMessage: 'I agree',
|
||||
description: '"I agree" button.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -6,6 +6,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Loading locked content messaging...',
|
||||
description: 'Message shown when an interface about locked content is being loaded',
|
||||
},
|
||||
'learn.loading.honor.code': {
|
||||
id: 'learn.loading.honor.codk',
|
||||
defaultMessage: 'Loading honor code messaging...',
|
||||
description: 'Message shown when an interface about the honor code is being loaded',
|
||||
},
|
||||
'learn.loading.learning.sequence': {
|
||||
id: 'learn.loading.learning.sequence',
|
||||
defaultMessage: 'Loading learning sequence...',
|
||||
|
||||
Reference in New Issue
Block a user