Merge pull request #19986 from open-craft/kshitij/report-status
Inline problem response report status
This commit is contained in:
@@ -18,7 +18,7 @@ export const buildBlockTree = (blocks, excludeBlockTypes) => {
|
||||
return blockTree(blocks.root, null);
|
||||
};
|
||||
|
||||
const blocks = (state = {}, action) => {
|
||||
export const blocks = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case courseBlocksActions.fetch.SUCCESS:
|
||||
return buildBlockTree(action.blocks, action.excludeBlockTypes);
|
||||
@@ -27,7 +27,7 @@ const blocks = (state = {}, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedBlock = (state = null, action) => {
|
||||
export const selectedBlock = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case courseBlocksActions.SELECT_BLOCK:
|
||||
return action.blockId;
|
||||
@@ -37,7 +37,7 @@ const selectedBlock = (state = null, action) => {
|
||||
};
|
||||
|
||||
|
||||
const rootBlock = (state = null, action) => {
|
||||
export const rootBlock = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case courseBlocksActions.fetch.SUCCESS:
|
||||
return action.blocks.root;
|
||||
|
||||
@@ -8,4 +8,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/prefer-default-export': 'off',
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
/* global gettext */
|
||||
import { Button } from '@edx/paragon';
|
||||
import { BlockBrowser } from 'BlockBrowser';
|
||||
import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { ReportStatusContainer } from '../ReportStatus/ReportStatusContainer';
|
||||
|
||||
export default class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleToggleDropdown = this.handleToggleDropdown.bind(this);
|
||||
this.initiateReportGeneration = this.initiateReportGeneration.bind(this);
|
||||
this.state = {
|
||||
showDropdown: false,
|
||||
};
|
||||
@@ -22,19 +24,39 @@ export default class Main extends React.Component {
|
||||
this.setState({ showDropdown: false });
|
||||
}
|
||||
|
||||
initiateReportGeneration() {
|
||||
this.props.createProblemResponsesReportTask(
|
||||
this.props.problemResponsesEndpoint,
|
||||
this.props.taskStatusEndpoint,
|
||||
this.props.selectedBlock,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedBlock, onSelectBlock } = this.props;
|
||||
|
||||
return (
|
||||
<div className="problem-browser">
|
||||
<Button onClick={this.handleToggleDropdown} label={gettext('Select a section or problem')} />
|
||||
<input type="text" name="problem-location" value={selectedBlock} disabled />
|
||||
{this.state.showDropdown &&
|
||||
<BlockBrowser onSelectBlock={(blockId) => {
|
||||
this.hideDropdown();
|
||||
onSelectBlock(blockId);
|
||||
}}
|
||||
/>}
|
||||
<div className="problem-browser-container">
|
||||
<div className="problem-browser">
|
||||
<Button
|
||||
onClick={this.handleToggleDropdown}
|
||||
label={gettext('Select a section or problem')}
|
||||
/>
|
||||
<input type="text" name="problem-location" value={selectedBlock} disabled />
|
||||
{this.state.showDropdown &&
|
||||
<BlockBrowserContainer
|
||||
onSelectBlock={(blockId) => {
|
||||
this.hideDropdown();
|
||||
onSelectBlock(blockId);
|
||||
}}
|
||||
/>}
|
||||
<Button
|
||||
onClick={this.initiateReportGeneration}
|
||||
name="list-problem-responses-csv"
|
||||
label={gettext('Create a report of problem responses')}
|
||||
/>
|
||||
</div>
|
||||
<ReportStatusContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,13 +64,17 @@ export default class Main extends React.Component {
|
||||
|
||||
Main.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
createProblemResponsesReportTask: PropTypes.func.isRequired,
|
||||
excludeBlockTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
fetchCourseBlocks: PropTypes.func.isRequired,
|
||||
problemResponsesEndpoint: PropTypes.string.isRequired,
|
||||
onSelectBlock: PropTypes.func.isRequired,
|
||||
selectedBlock: PropTypes.string,
|
||||
taskStatusEndpoint: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Main.defaultProps = {
|
||||
excludeBlockTypes: null,
|
||||
selectedBlock: null,
|
||||
selectedBlock: '',
|
||||
timeout: null,
|
||||
};
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
/* global jest,test,describe,expect */
|
||||
import { Button } from '@edx/paragon';
|
||||
import { BlockBrowser } from 'BlockBrowser';
|
||||
import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer';
|
||||
import { Provider } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import store from '../../data/store';
|
||||
|
||||
import Main from './Main';
|
||||
|
||||
describe('ProblemBrowser Main component', () => {
|
||||
const courseId = 'testcourse';
|
||||
const problemResponsesEndpoint = '/api/problem_responses/';
|
||||
const taskStatusEndpoint = '/api/task_status/';
|
||||
const excludedBlockTypes = [];
|
||||
|
||||
test('render with basic parameters', () => {
|
||||
const component = renderer.create(
|
||||
<Main
|
||||
courseId={courseId}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={jest.fn()}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={null}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<Main
|
||||
courseId={courseId}
|
||||
createProblemResponsesReportTask={jest.fn()}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={jest.fn()}
|
||||
problemResponsesEndpoint={problemResponsesEndpoint}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={null}
|
||||
taskStatusEndpoint={taskStatusEndpoint}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
@@ -27,13 +36,18 @@ describe('ProblemBrowser Main component', () => {
|
||||
|
||||
test('render with selected block', () => {
|
||||
const component = renderer.create(
|
||||
<Main
|
||||
courseId={courseId}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={jest.fn()}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={'some-selected-block'}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<Main
|
||||
courseId={courseId}
|
||||
createProblemResponsesReportTask={jest.fn()}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={jest.fn()}
|
||||
problemResponsesEndpoint={problemResponsesEndpoint}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={'some-selected-block'}
|
||||
taskStatusEndpoint={taskStatusEndpoint}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
@@ -42,15 +56,20 @@ describe('ProblemBrowser Main component', () => {
|
||||
test('fetch course block on toggling dropdown', () => {
|
||||
const fetchCourseBlocksMock = jest.fn();
|
||||
const component = renderer.create(
|
||||
<Main
|
||||
courseId={courseId}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={fetchCourseBlocksMock}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={'some-selected-block'}
|
||||
/>,
|
||||
<Provider store={store}>
|
||||
<Main
|
||||
courseId={courseId}
|
||||
createProblemResponsesReportTask={jest.fn()}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={fetchCourseBlocksMock}
|
||||
problemResponsesEndpoint={problemResponsesEndpoint}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={'some-selected-block'}
|
||||
taskStatusEndpoint={taskStatusEndpoint}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
const instance = component.getInstance();
|
||||
const instance = component.root.children[0].instance;
|
||||
instance.handleToggleDropdown();
|
||||
expect(fetchCourseBlocksMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
@@ -59,13 +78,17 @@ describe('ProblemBrowser Main component', () => {
|
||||
const component = shallow(
|
||||
<Main
|
||||
courseId={courseId}
|
||||
createProblemResponsesReportTask={jest.fn()}
|
||||
excludeBlockTypes={excludedBlockTypes}
|
||||
fetchCourseBlocks={jest.fn()}
|
||||
problemResponsesEndpoint={problemResponsesEndpoint}
|
||||
onSelectBlock={jest.fn()}
|
||||
selectedBlock={'some-selected-block'}
|
||||
taskStatusEndpoint={taskStatusEndpoint}
|
||||
/>,
|
||||
);
|
||||
component.find(Button).simulate('click');
|
||||
expect(component.find(BlockBrowser)).toBeTruthy();
|
||||
expect(component.find(BlockBrowserContainer).length).toBeFalsy();
|
||||
component.find(Button).find({ label: 'Select a section or problem' }).simulate('click');
|
||||
expect(component.find(BlockBrowserContainer).length).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fetchCourseBlocks, selectBlock } from 'BlockBrowser/data/actions/courseBlocks';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createProblemResponsesReportTask } from '../../data/actions/problemResponses';
|
||||
import Main from './Main';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
@@ -10,8 +10,16 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSelectBlock: blockId => dispatch(selectBlock(blockId)),
|
||||
fetchCourseBlocks: (courseId, excludeBlockTypes) =>
|
||||
dispatch(fetchCourseBlocks(courseId, excludeBlockTypes)),
|
||||
fetchCourseBlocks:
|
||||
(courseId, excludeBlockTypes) =>
|
||||
dispatch(fetchCourseBlocks(courseId, excludeBlockTypes)),
|
||||
createProblemResponsesReportTask:
|
||||
(problemResponsesEndpoint, taskStatusEndpoint, problemLocation) =>
|
||||
dispatch(
|
||||
createProblemResponsesReportTask(
|
||||
problemResponsesEndpoint, taskStatusEndpoint, problemLocation,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const MainContainer = connect(
|
||||
|
||||
@@ -2,44 +2,80 @@
|
||||
|
||||
exports[`ProblemBrowser Main component render with basic parameters 1`] = `
|
||||
<div
|
||||
className="problem-browser"
|
||||
className="problem-browser-container"
|
||||
>
|
||||
<button
|
||||
className="btn"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
className="problem-browser"
|
||||
>
|
||||
Select a section or problem
|
||||
</button>
|
||||
<input
|
||||
disabled={true}
|
||||
name="problem-location"
|
||||
type="text"
|
||||
value={null}
|
||||
<button
|
||||
className="btn"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Select a section or problem
|
||||
</button>
|
||||
<input
|
||||
disabled={true}
|
||||
name="problem-location"
|
||||
type="text"
|
||||
value={null}
|
||||
/>
|
||||
<button
|
||||
className="btn"
|
||||
name="list-problem-responses-csv"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Create a report of problem responses
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="report-generation-status"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ProblemBrowser Main component render with selected block 1`] = `
|
||||
<div
|
||||
className="problem-browser"
|
||||
className="problem-browser-container"
|
||||
>
|
||||
<button
|
||||
className="btn"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
className="problem-browser"
|
||||
>
|
||||
Select a section or problem
|
||||
</button>
|
||||
<input
|
||||
disabled={true}
|
||||
name="problem-location"
|
||||
type="text"
|
||||
value="some-selected-block"
|
||||
<button
|
||||
className="btn"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Select a section or problem
|
||||
</button>
|
||||
<input
|
||||
disabled={true}
|
||||
name="problem-location"
|
||||
type="text"
|
||||
value="some-selected-block"
|
||||
/>
|
||||
<button
|
||||
className="btn"
|
||||
name="list-problem-responses-csv"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Create a report of problem responses
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="report-generation-status"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/* global gettext */
|
||||
import { Icon } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
|
||||
const ReportStatus = ({ error, succeeded, inProgress, reportPath }) => {
|
||||
const progressMessage = (
|
||||
<div className="msg progress">
|
||||
{gettext('Your report is being generated...')}
|
||||
<Icon hidden className={['fa', 'fa-refresh', 'fa-spin', 'fa-fw']} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const successMessage = (
|
||||
<div className="msg success">
|
||||
{gettext('Your report has being successfully generated.')}
|
||||
<a href={reportPath}>
|
||||
<Icon hidden className={['fa', 'fa-link']} />
|
||||
{gettext('View Report')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const errorMessage = (
|
||||
<div className={classNames('msg', { error })}>
|
||||
{error && `${gettext('Error')}: `}
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="report-generation-status" aria-live="polite">
|
||||
{inProgress && progressMessage}
|
||||
{error && errorMessage}
|
||||
{succeeded && successMessage}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReportStatus.propTypes = {
|
||||
error: PropTypes.string,
|
||||
succeeded: PropTypes.bool.isRequired,
|
||||
inProgress: PropTypes.bool.isRequired,
|
||||
reportPath: PropTypes.string,
|
||||
};
|
||||
|
||||
ReportStatus.defaultProps = {
|
||||
error: null,
|
||||
reportPath: null,
|
||||
reportPreview: null,
|
||||
reportName: null,
|
||||
};
|
||||
|
||||
export default ReportStatus;
|
||||
@@ -0,0 +1,48 @@
|
||||
/* global test,describe,expect */
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import ReportStatus from './ReportStatus';
|
||||
|
||||
describe('ReportStatus component', () => {
|
||||
test('render in progress status', () => {
|
||||
const component = renderer.create(
|
||||
<ReportStatus
|
||||
error={null}
|
||||
inProgress
|
||||
reportName={null}
|
||||
reportPath={null}
|
||||
succeeded={false}
|
||||
/>,
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render success status', () => {
|
||||
const component = renderer.create(
|
||||
<ReportStatus
|
||||
error={null}
|
||||
inProgress={false}
|
||||
reportName={'some-report-name'}
|
||||
reportPath={'/some/report/path.csv'}
|
||||
succeeded
|
||||
/>,
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render error status', () => {
|
||||
const component = renderer.create(
|
||||
<ReportStatus
|
||||
error={'some error status'}
|
||||
inProgress={false}
|
||||
reportName={null}
|
||||
reportPath={null}
|
||||
succeeded={false}
|
||||
/>,
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ReportStatus from './ReportStatus';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
selectedBlock: state.selectedBlock,
|
||||
error: state.reportStatus.error,
|
||||
inProgress: state.reportStatus.inProgress,
|
||||
succeeded: state.reportStatus.succeeded,
|
||||
reportPath: state.reportStatus.reportPath,
|
||||
timeout: state.reportStatus.timeout,
|
||||
});
|
||||
|
||||
export const ReportStatusContainer = connect(
|
||||
mapStateToProps,
|
||||
)(ReportStatus);
|
||||
|
||||
export default ReportStatusContainer;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReportStatus component render error status 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="report-generation-status"
|
||||
>
|
||||
<div
|
||||
className="msg error"
|
||||
>
|
||||
Error:
|
||||
some error status
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ReportStatus component render in progress status 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="report-generation-status"
|
||||
>
|
||||
<div
|
||||
className="msg progress"
|
||||
>
|
||||
Your report is being generated...
|
||||
<div>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="fa fa-refresh fa-spin fa-fw"
|
||||
id="Icon2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ReportStatus component render success status 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="report-generation-status"
|
||||
>
|
||||
<div
|
||||
className="msg success"
|
||||
>
|
||||
Your report has being successfully generated.
|
||||
<a
|
||||
href="/some/report/path.csv"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="fa fa-link"
|
||||
id="Icon2"
|
||||
/>
|
||||
</div>
|
||||
View Report
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,4 @@
|
||||
export const REPORT_GENERATION_REQUEST = 'REPORT_GENERATION_REQUEST';
|
||||
export const REPORT_GENERATION_SUCCESS = 'REPORT_GENERATION_SUCCESS';
|
||||
export const REPORT_GENERATION_ERROR = 'REPORT_GENERATION_ERROR';
|
||||
export const REPORT_GENERATION_REFRESH_STATUS = 'REPORT_GENERATION_REFRESH_STATUS';
|
||||
@@ -0,0 +1,91 @@
|
||||
/* global gettext */
|
||||
import { fetchTaskStatus, initiateProblemResponsesRequest } from '../api/client';
|
||||
import {
|
||||
REPORT_GENERATION_ERROR,
|
||||
REPORT_GENERATION_REQUEST,
|
||||
REPORT_GENERATION_SUCCESS,
|
||||
REPORT_GENERATION_REFRESH_STATUS,
|
||||
} from './constants';
|
||||
|
||||
const taskStatusSuccess = (succeeded, inProgress, reportPath, reportName) => ({
|
||||
type: REPORT_GENERATION_SUCCESS,
|
||||
succeeded,
|
||||
inProgress,
|
||||
reportPath,
|
||||
reportName,
|
||||
});
|
||||
|
||||
const problemResponsesRequest = blockId => ({
|
||||
type: REPORT_GENERATION_REQUEST,
|
||||
blockId,
|
||||
});
|
||||
|
||||
const problemResponsesFailure = error => ({
|
||||
type: REPORT_GENERATION_ERROR,
|
||||
error,
|
||||
});
|
||||
|
||||
const problemResponsesRefreshStatus = timeout => ({
|
||||
type: REPORT_GENERATION_REFRESH_STATUS,
|
||||
timeout,
|
||||
});
|
||||
|
||||
const getTaskStatus = (endpoint, taskId) => dispatch =>
|
||||
fetchTaskStatus(endpoint, taskId)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error(response);
|
||||
})
|
||||
.then(
|
||||
(statusData) => {
|
||||
if (statusData.in_progress) {
|
||||
const timeout = setTimeout(() => dispatch(getTaskStatus(endpoint, taskId)), 2000);
|
||||
return dispatch(problemResponsesRefreshStatus(timeout));
|
||||
}
|
||||
if (statusData.task_state === 'SUCCESS') {
|
||||
const taskProgress = statusData.task_progress;
|
||||
const reportPath = taskProgress && taskProgress.report_path;
|
||||
const reportName = taskProgress && taskProgress.report_name;
|
||||
return dispatch(
|
||||
taskStatusSuccess(
|
||||
true,
|
||||
statusData.in_progress,
|
||||
reportPath,
|
||||
reportName,
|
||||
),
|
||||
);
|
||||
}
|
||||
return dispatch(problemResponsesFailure(gettext('There was an error generating your report.')));
|
||||
},
|
||||
() => dispatch(
|
||||
problemResponsesFailure(gettext('Unable to get report generation status.')),
|
||||
),
|
||||
);
|
||||
|
||||
const createProblemResponsesReportTask = (
|
||||
problemResponsesEndpoint,
|
||||
taskStatusEndpoint,
|
||||
blockId,
|
||||
) => (dispatch) => {
|
||||
dispatch(problemResponsesRequest(blockId));
|
||||
initiateProblemResponsesRequest(problemResponsesEndpoint, blockId)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error(response);
|
||||
})
|
||||
.then(
|
||||
json => dispatch(getTaskStatus(taskStatusEndpoint, json.task_id)),
|
||||
() => dispatch(problemResponsesFailure(gettext('Unable to submit request to generate report.'))),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
problemResponsesFailure,
|
||||
createProblemResponsesReportTask,
|
||||
problemResponsesRequest,
|
||||
getTaskStatus,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'whatwg-fetch';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const HEADERS = {
|
||||
Accept: 'application/json',
|
||||
'X-CSRFToken': Cookies.get('csrftoken'),
|
||||
};
|
||||
|
||||
function initiateProblemResponsesRequest(endpoint, blockId) {
|
||||
const formData = new FormData();
|
||||
formData.set('problem_location', blockId);
|
||||
|
||||
return fetch(
|
||||
endpoint, {
|
||||
credentials: 'same-origin',
|
||||
method: 'post',
|
||||
headers: HEADERS,
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fetchTaskStatus = (endpoint, taskId) => fetch(
|
||||
`${endpoint}/?task_id=${taskId}`, {
|
||||
credentials: 'same-origin',
|
||||
method: 'get',
|
||||
headers: HEADERS,
|
||||
});
|
||||
|
||||
export {
|
||||
initiateProblemResponsesRequest,
|
||||
fetchTaskStatus,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { combineReducers } from 'redux'; // eslint-disable-line
|
||||
import { blocks, selectedBlock, rootBlock } from 'BlockBrowser/data/reducers'; // eslint-disable-line
|
||||
import {
|
||||
REPORT_GENERATION_ERROR,
|
||||
REPORT_GENERATION_SUCCESS,
|
||||
REPORT_GENERATION_REFRESH_STATUS,
|
||||
REPORT_GENERATION_REQUEST,
|
||||
} from '../actions/constants';
|
||||
|
||||
const initialState = {
|
||||
error: null,
|
||||
inProgress: false,
|
||||
succeeded: false,
|
||||
reportPath: null,
|
||||
reportName: null,
|
||||
timeout: null,
|
||||
};
|
||||
|
||||
export const reportStatus = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case REPORT_GENERATION_REQUEST:
|
||||
return initialState;
|
||||
case REPORT_GENERATION_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
inProgress: action.inProgress,
|
||||
succeeded: action.succeeded,
|
||||
reportPath: action.reportPath,
|
||||
reportName: action.reportName,
|
||||
error: null,
|
||||
};
|
||||
case REPORT_GENERATION_ERROR:
|
||||
return { ...state, error: action.error, succeeded: false };
|
||||
case REPORT_GENERATION_REFRESH_STATUS:
|
||||
return { ...state, timeout: action.timeout };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
blocks,
|
||||
selectedBlock,
|
||||
rootBlock,
|
||||
reportStatus,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { applyMiddleware, createStore } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import rootReducer from './reducers';
|
||||
|
||||
const configureStore = initialState => createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
applyMiddleware(thunkMiddleware),
|
||||
);
|
||||
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
export default store;
|
||||
@@ -1,10 +1,10 @@
|
||||
import store from 'BlockBrowser/data/store';
|
||||
import React from 'react';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import store from './data/store';
|
||||
import MainContainer from './components/Main/MainContainer';
|
||||
|
||||
|
||||
export const ProblemBrowser = props => (
|
||||
<Provider store={store}>
|
||||
<MainContainer {...props} />
|
||||
|
||||
@@ -2564,7 +2564,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
|
||||
response = self.client.post(url, {'problem_location': problem_location})
|
||||
res_json = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(res_json, 'Could not find problem with this location.')
|
||||
self.assertEqual(res_json, "Could not find problem with this location.")
|
||||
|
||||
def valid_problem_location(test): # pylint: disable=no-self-argument
|
||||
"""
|
||||
@@ -2838,15 +2838,16 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
@ddt.data(*REPORTS_DATA)
|
||||
@ddt.unpack
|
||||
@valid_problem_location
|
||||
def test_calculate_report_csv_success(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs):
|
||||
def test_calculate_report_csv_success(
|
||||
self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs
|
||||
):
|
||||
kwargs = {'course_id': text_type(self.course.id)}
|
||||
kwargs.update(extra_instructor_api_kwargs)
|
||||
url = reverse(instructor_api_endpoint, kwargs=kwargs)
|
||||
success_status = u"The {report_type} report is being created.".format(report_type=report_type)
|
||||
with patch(task_api_endpoint) as patched_task_api_endpoint:
|
||||
patched_task_api_endpoint.return_value.task_id = "12345667-9abc-deff-ffed-cba987654321"
|
||||
|
||||
with patch(task_api_endpoint) as mock_task_api_endpoint:
|
||||
if report_type == 'problem responses':
|
||||
mock_task_api_endpoint.return_value = Mock(task_id='task-id-1138')
|
||||
response = self.client.post(url, {'problem_location': ''})
|
||||
self.assertContains(response, success_status)
|
||||
else:
|
||||
|
||||
@@ -973,7 +973,11 @@ class ProblemResponses(object):
|
||||
# Perform the upload
|
||||
problem_location = re.sub(r'[:/]', '_', problem_location)
|
||||
csv_name = 'student_state_from_{}'.format(problem_location)
|
||||
report_name = upload_csv_to_report_store(rows, csv_name, course_id, start_date)
|
||||
current_step = {'step': 'CSV uploaded', 'report_name': report_name}
|
||||
report_name, report_path = upload_csv_to_report_store(rows, csv_name, course_id, start_date)
|
||||
current_step = {
|
||||
'step': 'CSV uploaded',
|
||||
'report_name': report_name,
|
||||
'report_path': report_path,
|
||||
}
|
||||
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
@@ -44,8 +44,9 @@ def upload_csv_to_report_store(rows, csv_name, course_id, timestamp, config_name
|
||||
)
|
||||
|
||||
report_store.store_rows(course_id, report_name, rows)
|
||||
report_path = report_store.storage.url(report_store.path_to(course_id, report_name))
|
||||
tracker_emit(csv_name)
|
||||
return report_name
|
||||
return report_name, report_path
|
||||
|
||||
|
||||
def tracker_emit(report_name):
|
||||
|
||||
@@ -313,6 +313,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-generation-status {
|
||||
.msg {
|
||||
display: inherit;
|
||||
|
||||
&.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 0 1rem;
|
||||
|
||||
& > div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// elements - general
|
||||
@@ -1509,6 +1531,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#react-problem-report {
|
||||
margin: $baseline 0;
|
||||
}
|
||||
|
||||
.block-browser {
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -58,22 +58,18 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
</p>
|
||||
%endif
|
||||
|
||||
<p>
|
||||
<div class="problems">
|
||||
<div class="problems">
|
||||
${static.renderReact(
|
||||
component="ProblemBrowser",
|
||||
id="react-block-listing",
|
||||
id="react-problem-report",
|
||||
props={
|
||||
"courseId": course.id,
|
||||
"excludeBlockTypes": ['html', 'video', 'discussion']
|
||||
"excludeBlockTypes": ['html', 'video', 'discussion'],
|
||||
"problemResponsesEndpoint": section_data['get_problem_responses_url'],
|
||||
"taskStatusEndpoint": "/instructor_task_status"
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="button" name="list-problem-responses-csv" value="${_("Download a CSV of problem responses")}" data-endpoint="${ section_data['get_problem_responses_url'] }" data-csv="true">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="issued_certificates">
|
||||
<p>${_("Click to list certificates that are issued for this course:")}</p>
|
||||
|
||||
Reference in New Issue
Block a user