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:
Kshitij Sobti
2021-08-02 11:49:11 +05:30
committed by GitHub
parent b5f0af3f5c
commit 966b087b35
5 changed files with 110 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
});
});