diff --git a/lms/static/js/demographics_collection/DemographicsCollectionBanner.jsx b/lms/static/js/demographics_collection/DemographicsCollectionBanner.jsx
deleted file mode 100644
index d914d7fb03f7..000000000000
--- a/lms/static/js/demographics_collection/DemographicsCollectionBanner.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/* global gettext */
-import React from 'react';
-import Cookies from 'js-cookie';
-import {DemographicsCollectionModal} from './DemographicsCollectionModal';
-
-// eslint-disable-next-line import/prefer-default-export
-export class DemographicsCollectionBanner extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- modalOpen: false,
- hideBanner: false
- };
-
- this.dismissBanner = this.dismissBanner.bind(this);
- }
-
- /**
- * Utility function that controls hiding the CTA from the Course Dashboard where appropriate.
- * This can be called one of two ways - when a user clicks the "dismiss" button on the CTA
- * itself, or when the learner completes all of the questions within the modal.
- *
- * The dismiss button itself is nested inside of an , so we need to call stopPropagation()
- * here to prevent the Modal from _also_ opening when the Dismiss button is clicked.
- */
- async dismissBanner(e) {
- // Since this function also doubles as a callback in the Modal, we check if e is null/undefined
- // before calling stopPropagation()
- if (e) {
- e.stopPropagation();
- }
-
- const requestOptions = {
- method: 'PATCH',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFTOKEN': Cookies.get('csrftoken'),
- },
- body: JSON.stringify({
- show_call_to_action: false,
- })
- };
-
- await fetch(`${this.props.lmsRootUrl}/api/demographics/v1/demographics/status/`, requestOptions);
- // No matter what the response is from the API call we always allow the learner to dismiss the
- // banner when clicking the dismiss button
- this.setState({hideBanner: true});
- }
-
- render() {
- if (!(this.state.hideBanner)) {
- return (
-
- );
- } else {
- return null;
- }
- }
-}
diff --git a/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx b/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx
deleted file mode 100644
index bd9ff31828d1..000000000000
--- a/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx
+++ /dev/null
@@ -1,517 +0,0 @@
-/* global gettext */
-import React from 'react';
-// eslint-disable-next-line import/no-extraneous-dependencies
-import get from 'lodash/get';
-import Cookies from 'js-cookie';
-import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
-import FocusLock from 'react-focus-lock';
-import Wizard from './Wizard';
-import {SelectWithInput} from './SelectWithInput';
-import {MultiselectDropdown} from './MultiselectDropdown';
-import AxiosJwtTokenService from '../jwt_auth/AxiosJwtTokenService';
-import AxiosCsrfTokenService from '../jwt_auth/AxiosCsrfTokenService';
-
-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,
- // eslint-disable-next-line react/no-unused-state
- errorMessage: '',
- loading: true,
- // eslint-disable-next-line react/no-unused-state
- 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');
- }
-
- // eslint-disable-next-line react/sort-comp
- loadOptions(field) {
- const {choices} = get(this.state.options, field, {choices: []});
- if (choices.length) {
- // eslint-disable-next-line react/no-array-index-key
- return choices.map((choice, i) => {choice.display_name} );
- }
- }
-
- 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) {
- // eslint-disable-next-line react/no-unused-state
- 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) {
- // eslint-disable-next-line react/no-unused-state
- 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
- // eslint-disable-next-line react/no-unused-state
- 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) {
- // eslint-disable-next-line react/no-unused-state
- 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.')}
-
-
-
- {/* Need to strip out extra '"' characters in the marketingSiteBaseUrl prop or it tries to setup the href as a relative URL */}
- {/* eslint-disable-next-line react/jsx-no-target-blank */}
-
- {gettext('Why does edX collect this information?')}
-
-
- {this.state.fieldError &&
{gettext('An error occurred while attempting to retrieve or save the information below. Please try again later.')}
}
-
- )}
-
-
- {({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 */}
-
-
- {gettext('What was the total combined income, during the last 12 months, of all members of your family? ')}
-
-
- {gettext('Select income')}
- {
- this.loadOptions(FIELD_NAMES.INCOME)
- }
-
-
-
- )}
-
-
- {({wizardConsumer}) => (
-
- {/* Military History */}
-
-
- {gettext('Have you ever served on active duty in the U.S. Armed Forces, Reserves, or National Guard?')}
-
-
- {gettext('Select military status')}
- {
- this.loadOptions(FIELD_NAMES.MILITARY)
- }
-
-
-
- )}
-
-
- {({wizardConsumer}) => (
-
- {/* Learner Education Level */}
-
-
- {gettext('What is the highest level of education that you have achieved so far?')}
-
-
- {gettext('Select level of education')}
- {
- this.loadOptions(FIELD_NAMES.EDUCATION_LEVEL)
- }
-
-
- {/* Parent/Guardian Education Level */}
-
-
- {gettext('What is the highest level of education that any of your parents or guardians have achieved?')}
-
-
- {gettext('Select guardian education')}
- {
- this.loadOptions(FIELD_NAMES.PARENT_EDUCATION)
- }
-
-
-
- )}
-
-
- {({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 */}
-
-
- {gettext('What industry do you currently work in?')}
-
-
- {gettext('Select current industry')}
- {
- this.loadOptions(FIELD_NAMES.CURRENT_WORK)
- }
-
-
- {/* Future Work Industry */}
-
-
- {gettext('What industry do you want to work in?')}
-
-
- {gettext('Select prospective industry')}
- {
- this.loadOptions(FIELD_NAMES.FUTURE_WORK)
- }
-
-
-
- )}
-
-
-
-
-
- {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.')}
-
-
-
-
-
- );
- }
-}
-
-// eslint-disable-next-line import/prefer-default-export
-export {DemographicsCollectionModal};
diff --git a/lms/static/js/demographics_collection/MultiselectDropdown.jsx b/lms/static/js/demographics_collection/MultiselectDropdown.jsx
deleted file mode 100644
index b305f9f3484c..000000000000
--- a/lms/static/js/demographics_collection/MultiselectDropdown.jsx
+++ /dev/null
@@ -1,176 +0,0 @@
-/* global gettext */
-import React from 'react';
-import PropTypes from 'prop-types';
-
-class MultiselectDropdown extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- open: false,
- };
-
- // this version of React does not support React.createRef()
- this.buttonRef = null;
- this.setButtonRef = (element) => {
- this.buttonRef = element;
- };
-
- this.focusButton = this.focusButton.bind(this);
- this.handleKeydown = this.handleKeydown.bind(this);
- this.handleButtonClick = this.handleButtonClick.bind(this);
- this.handleRemoveAllClick = this.handleRemoveAllClick.bind(this);
- this.handleOptionClick = this.handleOptionClick.bind(this);
- }
-
- componentDidMount() {
- document.addEventListener('keydown', this.handleKeydown, false);
- }
-
- componentWillUnmount() {
- document.removeEventListener('keydown', this.handleKeydown, false);
- }
-
- // eslint-disable-next-line react/sort-comp
- findOption(data) {
- return this.props.options.find((o) => o.value == data || o.display_name == data);
- }
-
- focusButton() {
- if (this.buttonRef) { this.buttonRef.focus(); }
- }
-
- handleKeydown(event) {
- if (this.state.open && event.keyCode == 27) {
- this.setState({open: false}, this.focusButton);
- }
- }
-
- handleButtonClick(e) {
- // eslint-disable-next-line react/no-access-state-in-setstate
- this.setState({open: !this.state.open});
- }
-
- handleRemoveAllClick(e) {
- this.props.onChange([]);
- this.focusButton();
- e.stopPropagation();
- }
-
- handleOptionClick(e) {
- const value = e.target.value;
- const inSelected = this.props.selected.includes(value);
- let newSelected = [...this.props.selected];
-
- // if the option has its own onChange, trigger that instead
- if (this.findOption(value).onChange) {
- this.findOption(value).onChange(e.target.checked, value);
- return;
- }
-
- // if checked, add value to selected list
- if (e.target.checked && !inSelected) {
- newSelected = newSelected.concat(value);
- }
-
- // if unchecked, remove value from selected list
- if (!e.target.checked && inSelected) {
- newSelected = newSelected.filter(i => i !== value);
- }
-
- this.props.onChange(newSelected);
- }
-
- renderSelected() {
- if (this.props.selected.length == 0) {
- return this.props.emptyLabel;
- }
- const selectedList = this.props.selected
- .map(selected => this.findOption(selected).display_name)
- .join(', ');
- if (selectedList.length > 60) {
- return selectedList.substring(0, 55) + '...';
- }
- return selectedList;
- }
-
- renderUnselect() {
- return this.props.selected.length > 0 && (
- // eslint-disable-next-line react/button-has-type
- {gettext('Clear all')}
- );
- }
-
- renderMenu() {
- if (!this.state.open) {
- return;
- }
-
- const options = this.props.options.map((option, index) => {
- const checked = this.props.selected.includes(option.value);
- return (
- // eslint-disable-next-line react/no-array-index-key
-
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
-
-
- {option.display_name}
-
-
- );
- });
-
- return (
-
- {this.props.label}
- {options}
-
- );
- }
-
- render() {
- return (
- {
- // We need to make sure we only close and save the dropdown when
- // the user blurs on the parent to an element other than it's children.
- // essentially what this if statement is saying:
- // if the newly focused target is NOT a child of the this element, THEN fire the onBlur function
- // and close the dropdown.
- if (!e.currentTarget.contains(e.relatedTarget)) {
- this.props.onBlur(e);
- this.setState({open: false});
- }
- }}
- >
-
{this.props.label}
-
- {/* eslint-disable-next-line react/button-has-type */}
-
- {this.renderSelected()}
-
- {this.renderUnselect()}
-
-
- {this.renderMenu()}
-
-
- );
- }
-}
-
-// eslint-disable-next-line import/prefer-default-export
-export {MultiselectDropdown};
-
-MultiselectDropdown.propTypes = {
- // eslint-disable-next-line react/require-default-props
- label: PropTypes.string,
- // eslint-disable-next-line react/require-default-props
- emptyLabel: PropTypes.string,
- // eslint-disable-next-line react/forbid-prop-types
- options: PropTypes.array.isRequired,
- // eslint-disable-next-line react/forbid-prop-types
- selected: PropTypes.array.isRequired,
- onChange: PropTypes.func.isRequired,
-};
diff --git a/lms/static/js/demographics_collection/SelectWithInput.jsx b/lms/static/js/demographics_collection/SelectWithInput.jsx
deleted file mode 100644
index 408882c85f09..000000000000
--- a/lms/static/js/demographics_collection/SelectWithInput.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-
-// eslint-disable-next-line import/prefer-default-export, react/function-component-definition
-export const SelectWithInput = (props) => {
- const {
- selectName,
- selectId,
- selectValue,
- options,
- inputName,
- inputId,
- inputType,
- inputValue,
- selectOnChange,
- inputOnChange,
- showInput,
- inputOnBlur,
- labelText,
- disabled,
- } = props;
- return (
-
- {labelText}
-
- {options}
-
- {showInput
- && (
-
- )}
-
- );
-};
diff --git a/lms/static/js/demographics_collection/Wizard.jsx b/lms/static/js/demographics_collection/Wizard.jsx
deleted file mode 100644
index 48a2c8175ab4..000000000000
--- a/lms/static/js/demographics_collection/Wizard.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-/* global gettext */
-import React from 'react';
-// eslint-disable-next-line import/no-extraneous-dependencies
-import isFunction from 'lodash/isFunction';
-
-const Page = ({children}) => children;
-// eslint-disable-next-line react/function-component-definition
-const Header = () => null;
-// eslint-disable-next-line react/function-component-definition
-const Closer = () => null;
-// eslint-disable-next-line react/function-component-definition
-const ErrorPage = () => null;
-export default class Wizard extends React.Component {
- constructor(props) {
- super(props);
- this.findSubComponentByType = this.findSubComponentByType.bind(this);
- this.handleNext = this.handleNext.bind(this);
- this.state = {
- currentPage: 1,
- totalPages: 0,
- pages: [],
- // eslint-disable-next-line react/no-unused-state
- wizardContext: {},
- };
-
- this.wizardComplete = this.wizardComplete.bind(this);
- }
-
- componentDidMount() {
- const pages = this.findSubComponentByType(Wizard.Page.name);
- const totalPages = pages.length;
- const wizardContext = this.props.wizardContext;
- const closer = this.findSubComponentByType(Wizard.Closer.name)[0];
- pages.push(closer);
- // eslint-disable-next-line react/no-unused-state
- this.setState({pages, totalPages, wizardContext});
- }
-
- handleNext() {
- if (this.state.currentPage < this.props.children.length) {
- this.setState(prevState => ({currentPage: prevState.currentPage + 1}));
- }
- }
-
- findSubComponentByType(type) {
- return React.Children.toArray(this.props.children).filter(child => child.type.name === type);
- }
-
- // this needs to handle the case of no provided header
- // eslint-disable-next-line react/sort-comp
- renderHeader() {
- const header = this.findSubComponentByType(Wizard.Header.name)[0];
- return header.props.children({currentPage: this.state.currentPage, totalPages: this.state.totalPages});
- }
-
- renderPage() {
- if (this.state.totalPages) {
- const page = this.state.pages[this.state.currentPage - 1];
- if (page.type.name === Wizard.Closer.name) {
- return page.props.children;
- }
-
- if (isFunction(page.props.children)) {
- return page.props.children({wizardConsumer: this.props.wizardContext});
- } else {
- return page.props.children;
- }
- }
- return null;
- }
-
- // this needs to handle the case of no provided errorPage
- renderError() {
- const errorPage = this.findSubComponentByType(Wizard.ErrorPage.name)[0];
- return (
-
-
- {errorPage.props.children}
-
-
- {/* eslint-disable-next-line react/button-has-type, react/no-unknown-property */}
- {gettext('Close')}
-
-
- );
- }
-
- /**
- * Utility method that helps determine if the learner is on the final page of the modal.
- */
- onFinalPage() {
- return this.state.pages.length === this.state.currentPage;
- }
-
- /**
- * Utility method for closing the modal and returning the learner back to the Course Dashboard.
- * If a learner is on the final page of the modal, meaning they have answered all of the
- * questions, clicking the "Return to my dashboard" button will also dismiss the CTA from the
- * course dashboard.
- */
- async wizardComplete() {
- if (this.onFinalPage()) {
- this.props.dismissBanner();
- }
-
- this.props.onWizardComplete();
- }
-
- render() {
- const finalPage = this.onFinalPage();
- if (this.props.error) {
- return this.renderError();
- }
- return (
-
-
- {this.state.totalPages >= this.state.currentPage && this.renderHeader()}
-
- {this.renderPage()}
-
- {/* eslint-disable-next-line react/button-has-type */}
- {finalPage ? gettext('Return to my dashboard') : gettext('Finish later')}
- {/* eslint-disable-next-line react/button-has-type */}
- {gettext('Next')}
-
-
- );
- }
-}
-
-Wizard.Page = Page;
-Wizard.Header = Header;
-Wizard.Closer = Closer;
-Wizard.ErrorPage = ErrorPage;
diff --git a/lms/static/js/jwt_auth/.eslintrc.js b/lms/static/js/jwt_auth/.eslintrc.js
deleted file mode 100644
index 752a664a53ab..000000000000
--- a/lms/static/js/jwt_auth/.eslintrc.js
+++ /dev/null
@@ -1,18 +0,0 @@
-module.exports = {
- extends: '@edx/eslint-config',
- root: true,
- settings: {
- 'import/resolver': {
- webpack: {
- config: 'webpack.dev.config.js',
- },
- },
- },
- rules: {
- indent: ['error', 4],
- 'react/jsx-indent': ['error', 4],
- 'react/jsx-indent-props': ['error', 4],
- 'import/extensions': 'off',
- 'import/no-unresolved': 'off',
- },
-};
diff --git a/lms/static/js/jwt_auth/AxiosCsrfTokenService.js b/lms/static/js/jwt_auth/AxiosCsrfTokenService.js
deleted file mode 100644
index 20b9bc7d3ab7..000000000000
--- a/lms/static/js/jwt_auth/AxiosCsrfTokenService.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Service class to support CSRF.
- *
- * Temporarily copied from the edx/frontend-platform
- */
-import axios from 'axios';
-import { getUrlParts, processAxiosErrorAndThrow } from './utils';
-
-export default class AxiosCsrfTokenService {
- constructor(csrfTokenApiPath) {
- this.csrfTokenApiPath = csrfTokenApiPath;
- this.httpClient = axios.create();
- // Set withCredentials to true. Enables cross-site Access-Control requests
- // to be made using cookies, authorization headers or TLS client
- // certificates. More on MDN:
- // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
- this.httpClient.defaults.withCredentials = true;
- this.httpClient.defaults.headers.common['USE-JWT-COOKIE'] = true;
-
- this.csrfTokenCache = {};
- this.csrfTokenRequestPromises = {};
- }
-
- async getCsrfToken(url) {
- let urlParts;
- try {
- urlParts = getUrlParts(url);
- } catch (e) {
- // If the url is not parsable it's likely because a relative
- // path was supplied as the url. This is acceptable and in
- // this case we should use the current origin of the page.
- urlParts = getUrlParts(global.location.origin);
- }
- const { protocol, domain } = urlParts;
- const csrfToken = this.csrfTokenCache[domain];
-
- if (csrfToken) {
- return csrfToken;
- }
-
- if (!this.csrfTokenRequestPromises[domain]) {
- this.csrfTokenRequestPromises[domain] = this.httpClient
- .get(`${protocol}://${domain}${this.csrfTokenApiPath}`)
- .then((response) => {
- this.csrfTokenCache[domain] = response.data.csrfToken;
- return this.csrfTokenCache[domain];
- })
- .catch(processAxiosErrorAndThrow)
- .finally(() => {
- delete this.csrfTokenRequestPromises[domain];
- });
- }
-
- return this.csrfTokenRequestPromises[domain];
- }
-
- clearCsrfTokenCache() {
- this.csrfTokenCache = {};
- }
-
- getHttpClient() {
- return this.httpClient;
- }
-}
diff --git a/lms/static/js/jwt_auth/AxiosJwtTokenService.js b/lms/static/js/jwt_auth/AxiosJwtTokenService.js
deleted file mode 100644
index cb23b4fa6f03..000000000000
--- a/lms/static/js/jwt_auth/AxiosJwtTokenService.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Service class to support JWT Token Authentication.
- *
- * Temporarily copied from the edx/frontend-platform
- */
-import Cookies from 'universal-cookie';
-import jwtDecode from 'jwt-decode';
-import axios from 'axios';
-import createRetryInterceptor from './interceptors/createRetryInterceptor';
-import { processAxiosErrorAndThrow } from './utils';
-
-export default class AxiosJwtTokenService {
- static isTokenExpired(token) {
- return !token || token.exp < Date.now() / 1000;
- }
-
- constructor(tokenCookieName, tokenRefreshEndpoint) {
- this.tokenCookieName = tokenCookieName;
- this.tokenRefreshEndpoint = tokenRefreshEndpoint;
-
- this.httpClient = axios.create();
- // Set withCredentials to true. Enables cross-site Access-Control requests
- // to be made using cookies, authorization headers or TLS client
- // certificates. More on MDN:
- // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
- this.httpClient.defaults.withCredentials = true;
- // Add retries to this axios instance
- this.httpClient.interceptors.response.use(
- response => response,
- createRetryInterceptor({ httpClient: this.httpClient }),
- );
-
- this.cookies = new Cookies();
- this.refreshRequestPromises = {};
- }
-
- getHttpClient() {
- return this.httpClient;
- }
-
- decodeJwtCookie() {
- const cookieValue = this.cookies.get(this.tokenCookieName);
-
- if (cookieValue) {
- try {
- return jwtDecode(cookieValue);
- } catch (e) {
- const error = Object.create(e);
- error.message = 'Error decoding JWT token';
- error.customAttributes = { cookieValue };
- throw error;
- }
- }
-
- return null;
- }
-
- refresh() {
- if (this.refreshRequestPromises[this.tokenCookieName] === undefined) {
- const makeRefreshRequest = async () => {
- let axiosResponse;
- try {
- try {
- axiosResponse = await this.httpClient.post(this.tokenRefreshEndpoint);
- } catch (error) {
- processAxiosErrorAndThrow(error);
- }
- } catch (error) {
- const userIsUnauthenticated = error.response && error.response.status === 401;
- if (userIsUnauthenticated) {
- // Clean up the cookie if it exists to eliminate any situation
- // where the cookie is not expired but the jwt is expired.
- this.cookies.remove(this.tokenCookieName);
- const decodedJwtToken = null;
- return decodedJwtToken;
- }
-
- // TODO: Network timeouts and other problems will end up in
- // this block of code. We could add logic for retrying token
- // refreshes if we wanted to.
- throw error;
- }
-
- const decodedJwtToken = this.decodeJwtCookie();
-
- if (!decodedJwtToken) {
- // This is an unexpected case. The refresh endpoint should
- // set the cookie that is needed. See ARCH-948 for more
- // information on a similar situation that was happening
- // prior to this refactor in Oct 2019.
- const error = new Error('Access token is still null after successful refresh.');
- error.customAttributes = { axiosResponse };
- throw error;
- }
-
- return decodedJwtToken;
- };
-
- this.refreshRequestPromises[this.tokenCookieName] = makeRefreshRequest().finally(() => {
- delete this.refreshRequestPromises[this.tokenCookieName];
- });
- }
-
- return this.refreshRequestPromises[this.tokenCookieName];
- }
-
- async getJwtToken() {
- // eslint-disable-next-line no-useless-catch
- try {
- const decodedJwtToken = this.decodeJwtCookie(this.tokenCookieName);
- if (!AxiosJwtTokenService.isTokenExpired(decodedJwtToken)) {
- return decodedJwtToken;
- }
- } catch (e) {
- // Log unexpected error and continue with attempt to refresh it.
- throw e;
- }
-
- // eslint-disable-next-line no-useless-catch
- try {
- return await this.refresh();
- } catch (e) {
- throw e;
- }
- }
-}
diff --git a/lms/static/js/jwt_auth/README.rst b/lms/static/js/jwt_auth/README.rst
deleted file mode 100644
index 323d0d6e145f..000000000000
--- a/lms/static/js/jwt_auth/README.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-Responsibilities
-================
-The code in the jwt_auth folder was pulled from https://github.com/openedx/frontend-platform/tree/master/src/auth
-
-Primarily the code required to use https://github.com/openedx/frontend-platform/blob/master/src/auth/AxiosJwtTokenService.js
-
-This code will require updates if changes are made in the AxiosJwtTokenService.
-
-The responsibility of this code is to refresh and manage the JWT authentication token.
-It is included in all of our Micro Front-ends (MFE), but in edx-platform course
-dashboard and other frontend locations that are not yet in MFE form we still
-need to update the token to be able to call APIs in other IDAs.
-
-TODO: Investigate a long term approach to the JWT refresh issue in LMS https://openedx.atlassian.net/browse/MICROBA-548
diff --git a/lms/static/js/jwt_auth/interceptors/createRetryInterceptor.js b/lms/static/js/jwt_auth/interceptors/createRetryInterceptor.js
deleted file mode 100644
index e9cc20132241..000000000000
--- a/lms/static/js/jwt_auth/interceptors/createRetryInterceptor.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Interceptor class to support JWT Token Authentication.
- *
- * Temporarily copied from the edx/frontend-platform
- */
-import axios from 'axios';
-
-// This default algorithm is a recreation of what is documented here
-// https://cloud.google.com/storage/docs/exponential-backoff
-const defaultGetBackoffMilliseconds = (nthRetry, maximumBackoffMilliseconds = 16000) => {
- // Retry at exponential intervals (2, 4, 8, 16...)
- const exponentialBackoffSeconds = 2 ** nthRetry;
- // Add some randomness to avoid sending retries from separate requests all at once
- const randomFractionOfASecond = Math.random();
- const backoffSeconds = exponentialBackoffSeconds + randomFractionOfASecond;
- const backoffMilliseconds = Math.round(backoffSeconds * 1000);
- return Math.min(backoffMilliseconds, maximumBackoffMilliseconds);
-};
-
-const createRetryInterceptor = (options = {}) => {
- const {
- httpClient = axios.create(),
- getBackoffMilliseconds = defaultGetBackoffMilliseconds,
- // By default only retry outbound request failures (not responses)
- shouldRetry = (error) => {
- const isRequestError = !error.response && error.config;
- return isRequestError;
- },
- // A per-request maxRetries can be specified in request config.
- defaultMaxRetries = 2,
- } = options;
-
- const interceptor = async (error) => {
- const { config } = error;
-
- // If no config exists there was some other error setting up the request
- if (!config) {
- return Promise.reject(error);
- }
-
- if (!shouldRetry(error)) {
- return Promise.reject(error);
- }
-
- const {
- maxRetries = defaultMaxRetries,
- } = config;
-
- const retryRequest = async (nthRetry) => {
- if (nthRetry > maxRetries) {
- // Reject with the original error
- return Promise.reject(error);
- }
-
- let retryResponse;
-
- try {
- const backoffDelay = getBackoffMilliseconds(nthRetry);
- // Delay (wrapped in a promise so we can await the setTimeout)
- // eslint-disable-next-line no-promise-executor-return
- await new Promise(resolve => setTimeout(resolve, backoffDelay));
- // Make retry request
- retryResponse = await httpClient.request(config);
- } catch (e) {
- return retryRequest(nthRetry + 1);
- }
-
- return retryResponse;
- };
-
- return retryRequest(1);
- };
-
- return interceptor;
-};
-
-export default createRetryInterceptor;
-export { defaultGetBackoffMilliseconds };
diff --git a/lms/static/js/jwt_auth/utils.js b/lms/static/js/jwt_auth/utils.js
deleted file mode 100644
index 2bae53aabb36..000000000000
--- a/lms/static/js/jwt_auth/utils.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Utils file to support JWT Token Authentication.
- *
- * Temporarily copied from the edx/frontend-platform
- */
-
-// Lifted from here: https://regexr.com/3ok5o
-const urlRegex = /([a-z]{1,2}tps?):\/\/((?:(?!(?:\/|#|\?|&)).)+)(?:(\/(?:(?:(?:(?!(?:#|\?|&)).)+\/))?))?(?:((?:(?!(?:\.|$|\?|#)).)+))?(?:(\.(?:(?!(?:\?|$|#)).)+))?(?:(\?(?:(?!(?:$|#)).)+))?(?:(#.+))?/;
-const getUrlParts = (url) => {
- const found = url.match(urlRegex);
- try {
- const [
- fullUrl,
- protocol,
- domain,
- path,
- endFilename,
- endFileExtension,
- query,
- hash,
- ] = found;
-
- return {
- fullUrl,
- protocol,
- domain,
- path,
- endFilename,
- endFileExtension,
- query,
- hash,
- };
- } catch (e) {
- throw new Error(`Could not find url parts from ${url}.`);
- }
-};
-
-const logFrontendAuthError = (loggingService, error) => {
- const prefixedMessageError = Object.create(error);
- prefixedMessageError.message = `[frontend-auth] ${error.message}`;
- loggingService.logError(prefixedMessageError, prefixedMessageError.customAttributes);
-};
-
-const processAxiosError = (axiosErrorObject) => {
- const error = Object.create(axiosErrorObject);
- const { request, response, config } = error;
-
- if (!config) {
- error.customAttributes = {
- ...error.customAttributes,
- httpErrorType: 'unknown-api-request-error',
- };
- return error;
- }
-
- const {
- url: httpErrorRequestUrl,
- method: httpErrorRequestMethod,
- } = config;
- /* istanbul ignore else: difficult to enter the request-only error case in a unit test */
- if (response) {
- const { status, data } = response;
- const stringifiedData = JSON.stringify(data) || '(empty response)';
- const responseIsHTML = stringifiedData.includes('');
- // Don't include data if it is just an HTML document, like a 500 error page.
- /* istanbul ignore next */
- const httpErrorResponseData = responseIsHTML ? '' : stringifiedData;
- error.customAttributes = {
- ...error.customAttributes,
- httpErrorType: 'api-response-error',
- httpErrorStatus: status,
- httpErrorResponseData,
- httpErrorRequestUrl,
- httpErrorRequestMethod,
- };
- error.message = `Axios Error (Response): ${status} ${httpErrorRequestUrl} ${httpErrorResponseData}`;
- } else if (request) {
- error.customAttributes = {
- ...error.customAttributes,
- httpErrorType: 'api-request-error',
- httpErrorMessage: error.message,
- httpErrorRequestUrl,
- httpErrorRequestMethod,
- };
- // This case occurs most likely because of intermittent internet connection issues
- // but it also, though less often, catches CORS or server configuration problems.
- error.message = `Axios Error (Request): ${error.message} (possible local connectivity issue) ${httpErrorRequestMethod} ${httpErrorRequestUrl}`;
- } else {
- error.customAttributes = {
- ...error.customAttributes,
- httpErrorType: 'api-request-config-error',
- httpErrorMessage: error.message,
- httpErrorRequestUrl,
- httpErrorRequestMethod,
- };
- error.message = `Axios Error (Config): ${error.message} ${httpErrorRequestMethod} ${httpErrorRequestUrl}`;
- }
-
- return error;
-};
-
-const processAxiosErrorAndThrow = (axiosErrorObject) => {
- throw processAxiosError(axiosErrorObject);
-};
-
-export {
- getUrlParts,
- logFrontendAuthError,
- processAxiosError,
- processAxiosErrorAndThrow,
-};
diff --git a/package-lock.json b/package-lock.json
index 86f9197dc782..55c88fd9cc61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,6 @@
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
"@edx/paragon": "2.6.4",
"@edx/studio-frontend": "^2.1.0",
- "axios": "^0.28.0",
"babel-loader": "^9.1.3",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-polyfill": "6.26.0",
@@ -45,7 +44,6 @@
"jquery-migrate": "1.4.1",
"jquery.scrollto": "2.1.3",
"js-cookie": "3.0.5",
- "jwt-decode": "^3.1.2",
"moment": "2.30.1",
"moment-timezone": "0.5.45",
"node-gyp": "10.0.1",
@@ -71,7 +69,6 @@
"uglify-js": "2.7.0",
"underscore": "1.12.1",
"underscore.string": "3.3.6",
- "universal-cookie": "^4.0.4",
"webpack": "^5.90.3",
"webpack-bundle-tracker": "0.4.3",
"webpack-merge": "4.1.1",
@@ -5298,17 +5295,6 @@
"node": ">=4"
}
},
- "node_modules/axios": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz",
- "integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.0",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -10342,6 +10328,7 @@
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
+ "dev": true,
"funding": [
{
"type": "individual",
@@ -10429,19 +10416,6 @@
"node": "*"
}
},
- "node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/formatio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
@@ -15296,11 +15270,6 @@
"node": ">=4.0"
}
},
- "node_modules/jwt-decode": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
- "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
- },
"node_modules/karma": {
"version": "0.13.22",
"resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz",
@@ -19344,12 +19313,6 @@
"react": ">=0.14.0"
}
},
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
- },
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
diff --git a/package.json b/package.json
index 1879304bc192..4ec5c0d143f3 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
"@edx/paragon": "2.6.4",
"@edx/studio-frontend": "^2.1.0",
- "axios": "^0.28.0",
"babel-loader": "^9.1.3",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-polyfill": "6.26.0",
@@ -51,7 +50,6 @@
"jquery-migrate": "1.4.1",
"jquery.scrollto": "2.1.3",
"js-cookie": "3.0.5",
- "jwt-decode": "^3.1.2",
"moment": "2.30.1",
"moment-timezone": "0.5.45",
"node-gyp": "10.0.1",
@@ -77,7 +75,6 @@
"uglify-js": "2.7.0",
"underscore": "1.12.1",
"underscore.string": "3.3.6",
- "universal-cookie": "^4.0.4",
"webpack": "^5.90.3",
"webpack-bundle-tracker": "0.4.3",
"webpack-merge": "4.1.1",
diff --git a/webpack.common.config.js b/webpack.common.config.js
index 1ea0f5b5ea1c..322e252c6ae2 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -98,9 +98,6 @@ module.exports = Merge.smart({
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',
ProblemBrowser: './lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx',
- DemographicsCollectionBanner: './lms/static/js/demographics_collection/DemographicsCollectionBanner.jsx',
- DemographicsCollectionModal: './lms/static/js/demographics_collection/DemographicsCollectionModal.jsx',
- AxiosJwtTokenService: './lms/static/js/jwt_auth/AxiosJwtTokenService.js',
EnterpriseLearnerPortalModal: './lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx',
// Learner Dashboard