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:
Bianca Severino
2021-06-04 09:06:32 -04:00
committed by GitHub
parent 5bfca28450
commit 5c204ad0f9
15 changed files with 219 additions and 4 deletions

View File

@@ -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"

View File

@@ -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();

View 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);

View 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`);
});
});

View File

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

View 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;

View File

@@ -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...',