Make certain field uneditable for enterprise learners. (#23)

Make certain field uneditable for enterprise learners.
This commit is contained in:
Douglas Hall
2019-05-10 15:42:11 -04:00
committed by GitHub
8 changed files with 131 additions and 21 deletions

47
package-lock.json generated
View File

@@ -1026,9 +1026,9 @@
}
},
"@edx/frontend-auth": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-5.2.0.tgz",
"integrity": "sha512-/S5IMEIQXXUzXEQThZczWh4jRCyTvMB/oWFjMTbYr0WseyRhH2DWpy2X6WRgWObwnyiFRvYLzktIpcGoHLFA9A==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-5.3.0.tgz",
"integrity": "sha512-HiuwVlgePsBgQYlw47Z/lEKcmFrRbwYactOHMPXUgxaHKPXZwdVDvqBbf21/J/wPX0t1Kzmn0U5qfMd838hvYg==",
"requires": {
"@edx/frontend-logging": "^2.0.1",
"axios": "^0.18.0",
@@ -8582,7 +8582,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -8603,12 +8604,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -8623,17 +8626,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -8750,7 +8756,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -8762,6 +8769,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -8776,6 +8784,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -8783,12 +8792,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -8807,6 +8818,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -8887,7 +8899,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -8899,6 +8912,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -8984,7 +8998,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -9020,6 +9035,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -9039,6 +9055,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -9082,12 +9099,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},

View File

@@ -27,7 +27,7 @@
"@cospired/i18n-iso-languages": "^2.0.2",
"@edx/edx-bootstrap": "^2.1.0",
"@edx/frontend-analytics": "^1.0.0",
"@edx/frontend-auth": "^5.2.0",
"@edx/frontend-auth": "^5.3.0",
"@edx/frontend-component-footer": "^2.0.3",
"@edx/frontend-component-site-header": "^2.1.4",
"@edx/frontend-logging": "^2.0.0",

View File

@@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Hyperlink } from '@edx/paragon';
import { FormattedMessage } from 'react-intl';
import {
injectIntl,
intlShape,
@@ -8,10 +10,12 @@ import {
import messages from './AccountSettingsPage.messages';
import { configuration } from '../environment';
import { fetchSettings, saveSettings, updateDraft } from './actions';
import { accountSettingsPageSelector } from './selectors';
import { PageLoading } from '../common';
import Alert from './components/Alert';
import EditableField from './components/EditableField';
import PasswordReset from './components/PasswordReset';
import ThirdPartyAuth from './components/ThirdPartyAuth';
@@ -58,6 +62,36 @@ class AccountSettingsPage extends React.Component {
this.props.saveSettings(formId, values);
}
renderManagedProfileMessage() {
if (!this.props.profileDataManager) {
return (null);
}
return (
<div>
<Alert className="alert alert-primary" role="alert">
<FormattedMessage
id="account.settings.message.managed.settings"
defaultMessage="Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help."
description="alert message informing the user their account data is managed by a third party"
values={{
managerTitle: <b>{this.props.profileDataManager}</b>,
support: (
<Hyperlink href={configuration.SUPPORT_URL} target="_blank">
<FormattedMessage
id="account.settings.message.managed.settings.support"
defaultMessage="support"
description="website support"
/>
</Hyperlink>
),
}}
/>
</Alert>
</div>
);
}
renderContent() {
const editableFieldProps = {
onChange: this.handleEditableFieldChange,
@@ -72,6 +106,7 @@ class AccountSettingsPage extends React.Component {
<h2 className="h4">{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
{this.renderManagedProfileMessage()}
<EditableField
name="username"
@@ -88,6 +123,7 @@ class AccountSettingsPage extends React.Component {
value={this.props.formValues.name}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={!this.props.staticFields.includes('name')}
{...editableFieldProps}
/>
<EmailField
@@ -96,6 +132,7 @@ class AccountSettingsPage extends React.Component {
value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
isEditable={!this.props.staticFields.includes('email')}
{...editableFieldProps}
/>
<PasswordReset />
@@ -113,6 +150,7 @@ class AccountSettingsPage extends React.Component {
value={this.props.formValues.country}
options={this.props.countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
isEditable={!this.props.staticFields.includes('country')}
{...editableFieldProps}
/>
@@ -277,6 +315,8 @@ AccountSettingsPage.propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})),
profileDataManager: PropTypes.string,
staticFields: PropTypes.arrayOf(PropTypes.string),
fetchSiteLanguages: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
@@ -292,6 +332,8 @@ AccountSettingsPage.defaultProps = {
siteLanguageOptions: [],
countryOptions: [],
languageProficiencyOptions: [],
profileDataManager: null,
staticFields: [],
};

View File

@@ -21,9 +21,9 @@ export const fetchSettingsBegin = () => ({
type: FETCH_SETTINGS.BEGIN,
});
export const fetchSettingsSuccess = ({ values, thirdPartyAuthProviders }) => ({
export const fetchSettingsSuccess = ({ values, thirdPartyAuthProviders, profileDataManager }) => ({
type: FETCH_SETTINGS.SUCCESS,
payload: { values, thirdPartyAuthProviders },
payload: { values, thirdPartyAuthProviders, profileDataManager },
});
export const fetchSettingsFailure = error => ({

View File

@@ -37,6 +37,7 @@ const accountSettingsReducer = (state = defaultState, action) => {
...state,
values: Object.assign({}, state.values, action.payload.values),
authProviders: action.payload.thirdPartyAuthProviders,
profileDataManager: action.payload.profileDataManager,
loading: false,
loaded: true,
loadingError: null,

View File

@@ -18,7 +18,7 @@ import {
resetPasswordBegin,
resetPasswordSuccess,
} from './actions';
import { usernameSelector } from './selectors';
import { usernameSelector, userRolesSelector } from './selectors';
// Services
import * as ApiService from './service';
@@ -29,10 +29,15 @@ export function* handleFetchSettings() {
try {
yield put(fetchSettingsBegin());
const username = yield select(usernameSelector);
const userRoles = yield select(userRolesSelector);
const { thirdPartyAuthProviders, ...values } = yield call(ApiService.getSettings, username);
const {
thirdPartyAuthProviders,
profileDataManager,
...values
} = yield call(ApiService.getSettings, username, userRoles);
yield put(fetchSettingsSuccess({ values, thirdPartyAuthProviders }));
yield put(fetchSettingsSuccess({ values, thirdPartyAuthProviders, profileDataManager }));
} catch (e) {
logAPIErrorResponse(e);
yield put(fetchSettingsFailure(e.message));

View File

@@ -12,6 +12,8 @@ export const storeName = 'accountSettings';
export const usernameSelector = state => state.authentication.username;
export const userRolesSelector = state => state.authentication.roles || [];
export const accountSettingsSelector = state => ({ ...state[storeName] });
const editableFieldNameSelector = (state, props) => props.name;
@@ -74,6 +76,16 @@ export const thirdPartyAuthSelector = createSelector(
}),
);
export const profileDataManagerSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.profileDataManager,
);
export const staticFieldsSelector = createSelector(
accountSettingsSelector,
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
);
/**
* If there's no draft present at all (undefined), use the original committed value.
*/
@@ -123,6 +135,8 @@ export const accountSettingsPageSelector = createSelector(
languageProficiencyOptionsSelector,
formValuesSelector,
siteLanguageSelector,
profileDataManagerSelector,
staticFieldsSelector,
(
accountSettings,
siteLanguageOptions,
@@ -130,6 +144,8 @@ export const accountSettingsPageSelector = createSelector(
languageProficiencyOptions,
formValues,
siteLanguage,
profileDataManager,
staticFields,
) => ({
siteLanguageOptions,
countryOptions,
@@ -139,5 +155,7 @@ export const accountSettingsPageSelector = createSelector(
loaded: accountSettings.loaded,
loadingError: accountSettings.loadingError,
formValues,
profileDataManager,
staticFields,
}),
);

View File

@@ -128,21 +128,46 @@ export async function getThirdPartyAuthProviders() {
}));
}
/**
* Determine if the user's profile data is managed by a third-party identity provider.
*/
export async function getProfileDataManager(username, userRoles) {
const userRoleNames = userRoles.map(role => role.split(':')[0]);
if (userRoleNames.includes('enterprise_learner')) {
const url = `${config.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
const { data } = await apiClient.get(url).catch(handleRequestError);
if ('results' in data) {
for (let i = 0; i < data.results.length; i += 1) {
const enterprise = data.results[i].enterprise_customer;
if (enterprise.sync_learner_profile_data) {
return enterprise.name;
}
}
}
}
return null;
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
*/
export async function getSettings(username) {
export async function getSettings(username, userRoles) {
const results = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles),
]);
return {
...results[0],
...results[1],
thirdPartyAuthProviders: results[2],
profileDataManager: results[3],
};
}