/* global gettext */ import React from 'react'; import get from 'lodash/get'; import Wizard from './Wizard'; import Cookies from 'js-cookie'; import { SelectWithInput } from './SelectWithInput' import { MultiselectDropdown } from './MultiselectDropdown'; import AxiosJwtTokenService from '../jwt_auth/AxiosJwtTokenService'; import StringUtils from 'edx-ui-toolkit/js/utils/string-utils'; import AxiosCsrfTokenService from '../jwt_auth/AxiosCsrfTokenService'; import FocusLock from 'react-focus-lock'; const FIELD_NAMES = { CURRENT_WORK: "current_work_sector", FUTURE_WORK: "future_work_sector", GENDER: "gender", GENDER_DESCRIPTION: "gender_description", INCOME: "income", EDUCATION_LEVEL: "learner_education_level", MILITARY: "military_history", PARENT_EDUCATION: "parent_education_level", // For some reason, ethnicity has the really long property chain to get to the choices ETHNICITY_OPTIONS: "user_ethnicity.child.children.ethnicity", ETHNICITY: "user_ethnicity", WORK_STATUS: "work_status", WORK_STATUS_DESCRIPTION: "work_status_description", }; class DemographicsCollectionModal extends React.Component { constructor(props) { super(props); this.state = { options: {}, // a general error something goes really wrong error: false, // an error for when a specific demographics question fails to save fieldError: false, errorMessage: '', loading: true, open: this.props.open, selected: { [FIELD_NAMES.CURRENT_WORK]: '', [FIELD_NAMES.FUTURE_WORK]: '', [FIELD_NAMES.GENDER]: '', [FIELD_NAMES.GENDER_DESCRIPTION]: '', [FIELD_NAMES.INCOME]: '', [FIELD_NAMES.EDUCATION_LEVEL]: '', [FIELD_NAMES.MILITARY]: '', [FIELD_NAMES.PARENT_EDUCATION]: '', [FIELD_NAMES.ETHNICITY]: [], [FIELD_NAMES.WORK_STATUS]: '', [FIELD_NAMES.WORK_STATUS_DESCRIPTION]: '', } }; this.handleSelectChange = this.handleSelectChange.bind(this); this.handleMultiselectChange = this.handleMultiselectChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this); this.loadOptions = this.loadOptions.bind(this); this.getDemographicsQuestionOptions = this.getDemographicsQuestionOptions.bind(this); this.getDemographicsData = this.getDemographicsData.bind(this); // Get JWT token service to ensure the JWT token refreshes if needed const accessToken = this.props.jwtAuthToken; const refreshUrl = `${this.props.lmsRootUrl}/login_refresh`; this.jwtTokenService = new AxiosJwtTokenService( accessToken, refreshUrl, ); this.csrfTokenService = new AxiosCsrfTokenService(this.props.csrfTokenPath) } async componentDidMount() { // we add a class here to prevent scrolling on anything that is not the modal document.body.classList.add('modal-open'); const options = await this.getDemographicsQuestionOptions(); // gather previously answers questions const data = await this.getDemographicsData(); this.setState({ options: options.actions.POST, loading: false, selected: data }); } componentWillUnmount() { // remove the class to allow the dashboard content to scroll document.body.classList.remove('modal-open'); } loadOptions(field) { const { choices } = get(this.state.options, field, { choices: [] }); if (choices.length) { return choices.map((choice, i) => ); } } async handleSelectChange(e) { const url = `${this.props.demographicsBaseUrl}/demographics/api/v1/demographics/${this.props.user}/`; const name = e.target.name; const value = e.target.value; const options = { method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json', 'USE-JWT-COOKIE': true, 'X-CSRFToken': await this.retrieveDemographicsCsrfToken(url), }, body: JSON.stringify({ [name]: value === "default" ? null : value, }), }; try { await this.jwtTokenService.getJwtToken(); await fetch(url, options) } catch (error) { this.setState({ loading: false, fieldError: true, errorMessage: error }); } if (name === 'user_ethnicity') { return this.reduceEthnicityArray(value); } this.setState(prevState => ({ selected: { ...prevState.selected, [name]: value, } })); } handleMultiselectChange(values) { const decline = values.find(i => i === 'declined'); this.setState(({ selected }) => { // decline was previously selected if (selected[FIELD_NAMES.ETHNICITY].find(i => i === 'declined')) { return { selected: { ...selected, [FIELD_NAMES.ETHNICITY]: values.filter(value => value !== 'declined') } } // decline was just selected } else if (decline) { return { selected: { ...selected, [FIELD_NAMES.ETHNICITY]: [decline] } } // anything else was selected } else { return { selected: { ...selected, [FIELD_NAMES.ETHNICITY]: values } } } }); } handleInputChange(e) { const name = e.target.name; const value = e.target.value; this.setState(prevState => ({ selected: { ...prevState.selected, [name]: value, } })); } // We need to transform the ethnicity array before we POST or after GET the data to match // from [{ethnicity: 'example}] => to ['example'] // the format the UI requires the data to be in. reduceEthnicityArray(ethnicityArray) { return ethnicityArray.map((o) => o.ethnicity); } // Sets the CSRF token cookie to be used before each request that needs it. // if the cookie is already set, return it instead. We don't have to worry // about the cookie expiring, as it is tied to the session. async retrieveDemographicsCsrfToken(url) { let csrfToken = Cookies.get('demographics_csrftoken'); if (!csrfToken) { // set the csrf token cookie if not already set csrfToken = await this.csrfTokenService.getCsrfToken(url); Cookies.set('demographics_csrftoken', csrfToken); } return csrfToken; } // We gather the possible answers to any demographics questions from the OPTIONS of the api async getDemographicsQuestionOptions() { try { const optionsResponse = await fetch(`${this.props.demographicsBaseUrl}/demographics/api/v1/demographics/`, { method: 'OPTIONS' }) const demographicsOptions = await optionsResponse.json(); return demographicsOptions; } catch (error) { this.setState({ loading: false, error: true, errorMessage: error }); } } async getDemographicsData() { const requestOptions = { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json', 'USE-JWT-COOKIE': true }, }; let response; let data; try { await this.jwtTokenService.getJwtToken(); response = await fetch(`${this.props.demographicsBaseUrl}/demographics/api/v1/demographics/${this.props.user}/`, requestOptions); } catch (e) { // an error other than "no entry found" occured this.setState({ loading: false, error: true, errorMessage: e }); } // an entry was not found in demographics, so we need to create one if (response.status === 404) { data = await this.createDemographicsEntry(); return data; } // Otherwise, just return the data found data = await response.json(); if (data[FIELD_NAMES.ETHNICITY]) { // map ethnicity data to match what the UI requires data[FIELD_NAMES.ETHNICITY] = this.reduceEthnicityArray(data[FIELD_NAMES.ETHNICITY]); } return data; } async createDemographicsEntry() { const postUrl = `${this.props.demographicsBaseUrl}/demographics/api/v1/demographics/`; const postOptions = { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'USE-JWT-COOKIE': true, 'X-CSRFToken': await this.retrieveDemographicsCsrfToken(postUrl), }, body: JSON.stringify({ user: this.props.user, }), }; // Create the entry for the user try { const postResponse = await fetch(postUrl, postOptions); const data = await postResponse.json(); return data; } catch (e) { this.setState({ loading: false, error: true, errorMessage: e }); } } render() { if (this.state.loading) { return
} return (
{({ currentPage, totalPages }) => (

{StringUtils.interpolate( gettext('Section {currentPage} of {totalPages}'), { currentPage: currentPage, totalPages: totalPages } ) }

{gettext('Help make edX better for everyone!')}

{gettext('Welcome to edX! Before you get started, please take a few minutes to fill-in the additional information below to help us understand a bit more about your background. You can always edit this information later in Account Settings.')}


)}
{({ wizardConsumer }) =>
{/* Gender Identity */} {gettext("Select gender")}, this.loadOptions(FIELD_NAMES.GENDER) ]} showInput={wizardConsumer[FIELD_NAMES.GENDER] == "self-describe"} inputName={FIELD_NAMES.GENDER_DESCRIPTION} inputId={FIELD_NAMES.GENDER_DESCRIPTION} inputType="text" inputValue={wizardConsumer[FIELD_NAMES.GENDER_DESCRIPTION]} inputOnChange={this.handleInputChange} inputOnBlur={this.handleSelectChange} disabled={this.state.fieldError} /> {/* Ethnicity */} { // we create a fake "event", and then use it to call our normal selection handler function that // is used by the other dropdowns. const e = { target: { name: FIELD_NAMES.ETHNICITY, value: wizardConsumer[FIELD_NAMES.ETHNICITY].map(ethnicity => ({ ethnicity, value: ethnicity })), } } this.handleSelectChange(e); }} /> {/* Family Income */}
}
{({ wizardConsumer }) =>
{/* Military History */}
}
{({ wizardConsumer }) =>
{/* Learner Education Level */}
{/* Parent/Guardian Education Level */}
}
{({ wizardConsumer }) =>
{/* Employment Status */} {gettext("Select employment status")}, this.loadOptions(FIELD_NAMES.WORK_STATUS) ]} showInput={wizardConsumer[FIELD_NAMES.WORK_STATUS] == "other"} inputName={FIELD_NAMES.WORK_STATUS_DESCRIPTION} inputId={FIELD_NAMES.WORK_STATUS_DESCRIPTION} inputType="text" inputValue={wizardConsumer[FIELD_NAMES.WORK_STATUS_DESCRIPTION]} inputOnChange={this.handleInputChange} inputOnBlur={this.handleSelectChange} disabled={this.state.fieldError} /> {/* Current Work Industry */}
{/* Future Work Industry */}
}

{gettext("Thank you! You’re helping make edX better for everyone.")}

{this.state.error.length ? this.state.error : gettext("An error occurred while attempting to retrieve or save the information below. Please try again later.")}
) } } export { DemographicsCollectionModal };