feat: add support for enabling/disabling LTI PII sharing (#171)
Adds a new UI to enable/disable sharing of PII for discusission providers if PII sharing is enabled for the course.
This commit is contained in:
@@ -40,7 +40,7 @@ function PagesAndResources({ courseId, intl }) {
|
||||
return (
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<main className="container container-mw-md">
|
||||
<div className="d-flex justify-content-between my-5" style={{ 'align-items': 'center' }}>
|
||||
<div className="d-flex justify-content-between my-5 align-items-center">
|
||||
<h3 className="m-0">{intl.formatMessage(messages.heading)}</h3>
|
||||
<Hyperlink
|
||||
destination={lmsCourseURL}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Card, Form,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import AppExternalLinks from '../shared/AppExternalLinks';
|
||||
|
||||
import {
|
||||
updateValidationStatus,
|
||||
} from '../../../data/slice';
|
||||
import AppExternalLinks from '../shared/AppExternalLinks';
|
||||
import messages from './messages';
|
||||
|
||||
function LtiConfigForm({
|
||||
@@ -22,6 +23,8 @@ function LtiConfigForm({
|
||||
consumerKey: appConfig.consumerKey || '',
|
||||
consumerSecret: appConfig.consumerSecret || '',
|
||||
launchUrl: appConfig.launchUrl || '',
|
||||
piiShareUsername: appConfig.piiShareUsername,
|
||||
piiShareEmail: appConfig.piiShareEmail,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -39,6 +42,8 @@ function LtiConfigForm({
|
||||
consumerKey: Yup.string().required(intl.formatMessage(messages.consumerKeyRequired)),
|
||||
consumerSecret: Yup.string().required(intl.formatMessage(messages.consumerSecretRequired)),
|
||||
launchUrl: Yup.string().required(intl.formatMessage(messages.launchUrlRequired)),
|
||||
piiShareUsername: Yup.bool(),
|
||||
piiShareEmail: Yup.bool(),
|
||||
}),
|
||||
onSubmit,
|
||||
});
|
||||
@@ -67,9 +72,9 @@ function LtiConfigForm({
|
||||
value={values.consumerKey}
|
||||
/>
|
||||
{isInvalidConsumerKey && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<span className="x-small">{errors.consumerKey}</span>
|
||||
</Form.Control.Feedback>
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<span className="x-small">{errors.consumerKey}</span>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="consumerSecret" isInvalid={isInvalidConsumerSecret} className="mb-4">
|
||||
@@ -80,9 +85,9 @@ function LtiConfigForm({
|
||||
value={values.consumerSecret}
|
||||
/>
|
||||
{isInvalidConsumerSecret && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<span className="x-small">{errors.consumerSecret}</span>
|
||||
</Form.Control.Feedback>
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<span className="x-small">{errors.consumerSecret}</span>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="launchUrl" isInvalid={isInvalidLaunchUrl}>
|
||||
@@ -93,11 +98,36 @@ function LtiConfigForm({
|
||||
value={values.launchUrl}
|
||||
/>
|
||||
{isInvalidLaunchUrl && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<span className="x-small">{errors.launchUrl}</span>
|
||||
</Form.Control.Feedback>
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
<span className="x-small">{errors.launchUrl}</span>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
{appConfig.piiSharing && (
|
||||
<>
|
||||
<Form.Text className="my-2">
|
||||
{intl.formatMessage(messages.piiSharing)}
|
||||
</Form.Text>
|
||||
<Form.Group controlId="piiSharing">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
name="piiShareUsername"
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
checked={values.piiShareUsername}
|
||||
label={intl.formatMessage(messages.piiShareUsername)}
|
||||
/>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
name="piiShareEmail"
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
checked={values.piiShareEmail}
|
||||
label={intl.formatMessage(messages.piiShareEmail)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
<AppExternalLinks externalLinks={externalLinks} title={title} />
|
||||
</Card>
|
||||
@@ -120,6 +150,9 @@ LtiConfigForm.propTypes = {
|
||||
consumerKey: PropTypes.string,
|
||||
consumerSecret: PropTypes.string,
|
||||
launchUrl: PropTypes.string,
|
||||
piiSharing: PropTypes.bool.isRequired,
|
||||
piiShareUsername: PropTypes.bool.isRequired,
|
||||
piiShareEmail: PropTypes.bool.isRequired,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
|
||||
@@ -39,6 +39,20 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Launch URL is a required field',
|
||||
description: 'Tells the user that the Launch URL field is required and must have a value.',
|
||||
},
|
||||
piiSharing: {
|
||||
id: 'authoring.discussions.piiSharing',
|
||||
defaultMessage: 'Optionally share a user\'s username and/or email with the LTI provider:',
|
||||
},
|
||||
piiShareUsername: {
|
||||
id: 'authoring.discussions.piiShareUsername',
|
||||
defaultMessage: 'Share username',
|
||||
description: 'Label for the Share Username field.',
|
||||
},
|
||||
piiShareEmail: {
|
||||
id: 'authoring.discussions.piiShareEmail',
|
||||
defaultMessage: 'Share email',
|
||||
description: 'Label for the Share Email field.',
|
||||
},
|
||||
contact: {
|
||||
id: 'authoring.discussions.appDocInstructions.contact',
|
||||
defaultMessage: 'Contact: {link}',
|
||||
|
||||
@@ -11,6 +11,9 @@ function normalizeLtiConfig(data) {
|
||||
consumerKey: data.lti_1p1_client_key,
|
||||
consumerSecret: data.lti_1p1_client_secret,
|
||||
launchUrl: data.lti_1p1_launch_url,
|
||||
piiSharing: 'pii_share_username' in data || 'pii_share_email' in data,
|
||||
piiShareUsername: data.pii_share_username,
|
||||
piiShareEmail: data.pii_share_email,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +121,12 @@ function denormalizeData(courseId, appId, data) {
|
||||
if (data.launchUrl) {
|
||||
ltiConfiguration.lti_1p1_launch_url = data.launchUrl;
|
||||
}
|
||||
if ('piiShareUsername' in data) {
|
||||
ltiConfiguration.pii_share_username = data.piiShareUsername;
|
||||
}
|
||||
if ('piiShareEmail' in data) {
|
||||
ltiConfiguration.pii_share_email = data.piiShareEmail;
|
||||
}
|
||||
|
||||
if (Object.keys(ltiConfiguration).length > 0) {
|
||||
// Only add this in if we're sending LTI fields.
|
||||
|
||||
@@ -166,6 +166,45 @@ describe('Data layer integration tests', () => {
|
||||
consumerKey: 'client_key_123',
|
||||
consumerSecret: 'client_secret_123',
|
||||
launchUrl: 'https://localhost/example',
|
||||
piiSharing: false,
|
||||
piiShareUsername: undefined,
|
||||
piiShareEmail: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully loads an LTI configuration with PII Sharing', async () => {
|
||||
axiosMock.onGet(getAppsUrl(courseId)).reply(200, {
|
||||
...piazzaApiResponse,
|
||||
lti_configuration: {
|
||||
...piazzaApiResponse.lti_configuration,
|
||||
pii_share_username: true,
|
||||
pii_share_email: false,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(fetchApps(courseId), store.dispatch);
|
||||
|
||||
expect(store.getState().discussions).toEqual({
|
||||
appIds: ['legacy', 'piazza'],
|
||||
featureIds,
|
||||
activeAppId: 'piazza',
|
||||
selectedAppId: null,
|
||||
status: LOADED,
|
||||
saveStatus: SAVED,
|
||||
hasValidationError: false,
|
||||
discussionTopicIds: [],
|
||||
});
|
||||
expect(store.getState().models.apps.legacy).toEqual(legacyApp);
|
||||
expect(store.getState().models.apps.piazza).toEqual(piazzaApp);
|
||||
expect(store.getState().models.features).toEqual(featuresState);
|
||||
expect(store.getState().models.appConfigs.piazza).toEqual({
|
||||
id: 'piazza',
|
||||
consumerKey: 'client_key_123',
|
||||
consumerSecret: 'client_secret_123',
|
||||
launchUrl: 'https://localhost/example',
|
||||
piiSharing: true,
|
||||
piiShareUsername: true,
|
||||
piiShareEmail: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,6 +368,9 @@ describe('Data layer integration tests', () => {
|
||||
consumerKey: 'new_consumer_key',
|
||||
consumerSecret: 'new_consumer_secret',
|
||||
launchUrl: 'https://localhost/new_launch_url',
|
||||
piiSharing: false,
|
||||
piiShareUsername: undefined,
|
||||
piiShareEmail: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user