diff --git a/src/account-settings/data/selectors.js b/src/account-settings/data/selectors.js
index d441dd2..ac29cad 100644
--- a/src/account-settings/data/selectors.js
+++ b/src/account-settings/data/selectors.js
@@ -1,6 +1,5 @@
import { createSelector, createStructuredSelector } from 'reselect';
-
-import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
+import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
export const storeName = 'accountSettings';
@@ -195,11 +194,14 @@ export const coachingConsentPageSelector = createSelector(
export const demographicsSectionSelector = createSelector(
formValuesSelector,
draftsSelector,
+ errorSelector,
(
formValues,
drafts,
+ errors,
) => ({
formValues,
drafts,
+ formErrors: errors,
}),
);
diff --git a/src/account-settings/demographics/Checkboxes.jsx b/src/account-settings/demographics/Checkboxes.jsx
index 4a037f1..d593404 100644
--- a/src/account-settings/demographics/Checkboxes.jsx
+++ b/src/account-settings/demographics/Checkboxes.jsx
@@ -29,7 +29,7 @@ export const Checkboxes = (props) => {
setSelected(newSelected);
}
- // If unchecked, make sure this option is NOT in `seleted`
+ // If unchecked, make sure this option is NOT in `selected`
if (!value) {
setSelected(selected.filter(i => i !== option));
}
diff --git a/src/account-settings/demographics/DemographicsSection.jsx b/src/account-settings/demographics/DemographicsSection.jsx
index be6167f..b1c3543 100644
--- a/src/account-settings/demographics/DemographicsSection.jsx
+++ b/src/account-settings/demographics/DemographicsSection.jsx
@@ -1,31 +1,46 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Input } from '@edx/paragon';
-import memoize from 'memoize-one';
-
-import { demographicsSectionSelector } from '../data/selectors';
-import { saveMultipleSettings, updateDraft } from '../data/actions';
-import EditableField from '../EditableField';
-import Checkboxes from './Checkboxes';
-import messages from './DemographicsSection.messages';
import {
- SELF_DESCRIBE,
- DEMOGRAPHICS_GENDER_OPTIONS,
+ DECLINED,
+ DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS,
DEMOGRAPHICS_ETHNICITY_OPTIONS,
+ DEMOGRAPHICS_GENDER_OPTIONS,
DEMOGRAPHICS_INCOME_OPTIONS,
DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS,
- DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS,
- OTHER,
- DEMOGRAPHICS_WORK_STATUS_OPTIONS,
DEMOGRAPHICS_WORK_SECTOR_OPTIONS,
- DECLINED,
+ DEMOGRAPHICS_WORK_STATUS_OPTIONS,
+ OTHER,
+ SELF_DESCRIBE,
} from '../data/constants';
+import {
+ FormattedMessage,
+ injectIntl,
+ intlShape,
+} from '@edx/frontend-platform/i18n';
+import { saveMultipleSettings, updateDraft } from '../data/actions';
+
+import Alert from '../Alert';
+import Checkboxes from './Checkboxes';
+import EditableField from '../EditableField';
+import { Input } from '@edx/paragon';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+import { demographicsSectionSelector } from '../data/selectors';
+import get from 'lodash.get';
+import isEmpty from 'lodash.isempty';
+import memoize from 'memoize-one';
+import messages from './DemographicsSection.messages';
class DemographicsSection extends React.Component {
constructor(props, context) {
- super(props, context)
+ super(props, context);
+
+ this.alertRef = React.createRef();
+ }
+
+ componentDidUpdate() {
+ if(!isEmpty(this.props.formErrors)) {
+ this.alertRef.current.focus();
+ }
}
getLocalizedOptions = memoize((locale) => ({
@@ -67,13 +82,15 @@ class DemographicsSection extends React.Component {
}
ethnicityFieldDisplay = () => {
- const ethnicities = this.props.formValues.demographics_user_ethnicity;
- return ethnicities.map((e) => {
- if (e == DECLINED) {
- return this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`]);
- }
- return this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${e}`])
- }).join(", ")
+ if (get(this, 'props.formValues.demographics_user_ethnicity')) {
+ const ethnicities = this.props.formValues.demographics_user_ethnicity;
+ return ethnicities.map((e) => {
+ if (e == DECLINED) {
+ return this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`]);
+ }
+ return this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${e}`]);
+ }).join(", ")
+ }
}
handleEditableFieldChange = (name, value) => {
@@ -95,6 +112,31 @@ class DemographicsSection extends React.Component {
this.props.saveMultipleSettings(settingsArray, formId);
};
+ /**
+ * If an error is encountered when trying to communicate with the Demographics IDA then we will
+ * display an Alert letting the user know that their info will not be retrieved or displayed
+ * and temporarily cannot be updated.
+ */
+ renderDemographicsServiceIssueWarning() {
+ if (!isEmpty(this.props.formErrors)) {
+ return (
+
+ );
+ } else {
+ return null;
+ }
+ }
+
render() {
const editableFieldProps = {
onChange: this.handleEditableFieldChange,
@@ -119,6 +161,7 @@ class DemographicsSection extends React.Component {
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
+ {this.renderDemographicsServiceIssueWarning()}
)
}
-}
+};
DemographicsSection.propTypes = {
intl: intlShape.isRequired,
@@ -254,10 +297,13 @@ DemographicsSection.propTypes = {
demographics_current_work_sector: PropTypes.string,
demographics_future_work_sector: PropTypes.string,
}).isRequired,
+ formErrors: PropTypes.shape({
+ demographicsError: PropTypes.string,
+ }).isRequired,
updateDraft: PropTypes.func.isRequired
};
export default connect(demographicsSectionSelector, {
saveMultipleSettings,
updateDraft,
-})(injectIntl(DemographicsSection))
+})(injectIntl(DemographicsSection));
diff --git a/src/account-settings/demographics/data/service.js b/src/account-settings/demographics/data/service.js
index 69ed3e0..c2f1dd4 100644
--- a/src/account-settings/demographics/data/service.js
+++ b/src/account-settings/demographics/data/service.js
@@ -2,6 +2,23 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { convertData, TO, FROM } from './utils';
+/**
+ * If there was an error making the PATCH call then we create an apiError object containing a
+ * 'demographicsError' fieldError. The content of the error itself isn't particularly important.
+ * This will trigger the `renderDemographicsServiceIssueWarningMessage()` (in
+ * DemographicsSection.jsx) to display an Alert to let the end-user know that there may be an
+ * issue communicating with the Demographics service.
+ *
+ * @param {Error} error
+ */
+export function createDemographicsError(error) {
+ const apiError = Object.create(error);
+ apiError.fieldErrors = {
+ demographicsError: error.customAttributes.httpErrorType,
+ };
+ return apiError;
+}
+
/**
* post all of the data related to demographics.
* @param {Number} userId users are identified in the api by LMS id
@@ -15,7 +32,7 @@ export async function postDemographics(userId) {
({ data } = await getAuthenticatedHttpClient()
.post(requestUrl, commitValues)
.catch((error) => {
- const apiError = Object.create(error);
+ const apiError = createDemographicsError(error);
throw apiError;
}));
@@ -36,7 +53,29 @@ export async function getDemographics(userId) {
data = convertData(data, FROM);
} catch (error) {
- data = await postDemographics(userId);
+ const apiError = Object.create(error);
+ // if the API called resulted in this user receiving a 404 then follow up with a POST call to
+ // try and create the demographics entity on the backend
+ if (apiError.customAttributes.httpErrorStatus) {
+ if (apiError.customAttributes.httpErrorStatus === 404) {
+ data = await postDemographics(userId);
+ }
+ } else {
+ data = {
+ user: userId,
+ demographics_gender: '',
+ demographics_gender_description: '',
+ demographics_income: '',
+ demographics_learner_education_level: '',
+ demographics_parent_education_level: '',
+ demographics_military_history: '',
+ demographics_work_status: '',
+ demographics_work_status_description: '',
+ demographics_current_work_sector: '',
+ demographics_future_work_sector: '',
+ demographics_user_ethnicity: [],
+ };
+ }
}
return data;
@@ -55,7 +94,7 @@ export async function patchDemographics(userId, commitValues) {
({ data } = await getAuthenticatedHttpClient()
.patch(requestUrl, convertedCommitValues)
.catch((error) => {
- const apiError = Object.create(error);
+ const apiError = createDemographicsError(error);
throw apiError;
}));
diff --git a/src/account-settings/demographics/test/DemographicsSection.test.jsx b/src/account-settings/demographics/test/DemographicsSection.test.jsx
new file mode 100644
index 0000000..bf99236
--- /dev/null
+++ b/src/account-settings/demographics/test/DemographicsSection.test.jsx
@@ -0,0 +1,128 @@
+import * as auth from '@edx/frontend-platform/auth';
+import * as selectors from '../../data/selectors';
+
+import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
+
+import DemographicsSection from '../DemographicsSection';
+import { Provider } from 'react-redux';
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import renderer from 'react-test-renderer';
+
+jest.mock('@edx/frontend-platform/auth');
+
+const IntlDemographicsSection = injectIntl(DemographicsSection);
+
+jest.mock('../../data/selectors', () => {
+ return jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) }));
+});
+
+const mockStore = configureStore();
+
+describe('DemographicsSection', () => {
+ let props = {};
+ let store = {};
+
+ const reduxWrapper = children => (
+
+ {children}
+
+ );
+
+ beforeEach(() => {
+ store = mockStore();
+ props = {
+ updateDraft: jest.fn(),
+ formValues: {
+ demographics_gender: 'declined',
+ demographics_gender_description: '',
+ demographics_user_ethnicity: [],
+ demographics_income: 'declined',
+ demographics_military_history: 'declined',
+ demographics_learner_education_level: 'declined',
+ demographics_parent_education_level: 'declined',
+ demographics_work_status: 'declined',
+ demographics_work_status_description: '',
+ demographics_current_work_sector: 'declined',
+ demographics_future_work_sector: 'declined',
+ demographics_user: 1,
+ },
+ formErrors: {},
+ intl: {},
+ };
+ auth.getAuthenticatedHttpClient = jest.fn(() => ({
+ patch: async () => ({
+ data: { status: 200 },
+ catch: () => {},
+ }),
+ }));
+ auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
+ });
+
+ it('should render', () => {
+ const wrapper = renderer.create(reduxWrapper()).toJSON();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render an Alert if an error occurs', () => {
+ props = {
+ ...props,
+ formErrors: {
+ demographicsError: "api-error"
+ }
+ };
+
+ const wrapper = renderer.create(reduxWrapper()).toJSON();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should set user input correctly when user provides gender self-description', () => {
+ props = {
+ ...props,
+ formValues: {
+ demographics_gender: 'self-describe',
+ demographics_gender_description: 'test',
+ },
+ };
+
+ const wrapper = renderer.create(reduxWrapper()).toJSON();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should set user input correctly when user provides answers to work_status question', () => {
+ props = {
+ ...props,
+ formValues: {
+ demographics_work_status: 'other',
+ demographics_work_status_description: 'test',
+ }
+ }
+
+ const wrapper = renderer.create(reduxWrapper()).toJSON();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render ethnicity text correctly', () => {
+ props = {
+ ...props,
+ formValues: {
+ demographics_user_ethnicity: ['asian']
+ }
+ }
+
+ const wrapper = renderer.create(reduxWrapper()).toJSON();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render ethnicity correctly when multiple options are selected', () => {
+ props = {
+ ...props,
+ formValues: {
+ demographics_user_ethnicity: ['hispanic-latin-spanish', 'white']
+ }
+ }
+
+ const wrapper = renderer.create(reduxWrapper()).toJSON();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/src/account-settings/demographics/test/__snapshots__/DemographicsSection.test.jsx.snap b/src/account-settings/demographics/test/__snapshots__/DemographicsSection.test.jsx.snap
new file mode 100644
index 0000000..9e2e422
--- /dev/null
+++ b/src/account-settings/demographics/test/__snapshots__/DemographicsSection.test.jsx.snap
@@ -0,0 +1,3713 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DemographicsSection should render 1`] = `
+
+`;
+
+exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
+
+`;
+
+exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
+
+`;
+
+exports[`DemographicsSection should render ethnicity text correctly 1`] = `
+
+`;
+
+exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
+
+`;
+
+exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
+
+`;