diff --git a/common/static/common/js/components/BlockBrowser/data/reducers/index.js b/common/static/common/js/components/BlockBrowser/data/reducers/index.js
index 7d14e63052cd..365e9153edd5 100644
--- a/common/static/common/js/components/BlockBrowser/data/reducers/index.js
+++ b/common/static/common/js/components/BlockBrowser/data/reducers/index.js
@@ -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;
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/.eslintrc.js b/lms/djangoapps/instructor/static/instructor/.eslintrc.js
similarity index 75%
rename from lms/djangoapps/instructor/static/instructor/ProblemBrowser/.eslintrc.js
rename to lms/djangoapps/instructor/static/instructor/.eslintrc.js
index 838b853a8277..23fa913be312 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/.eslintrc.js
+++ b/lms/djangoapps/instructor/static/instructor/.eslintrc.js
@@ -8,4 +8,7 @@ module.exports = {
},
},
},
+ rules: {
+ 'import/prefer-default-export': 'off',
+ },
};
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx
index aca0041c9948..a9c9c5c61dd9 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx
@@ -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 (
-
-
-
- {this.state.showDropdown &&
-
{
- this.hideDropdown();
- onSelectBlock(blockId);
- }}
- />}
+
+
+
+
+ {this.state.showDropdown &&
+ {
+ this.hideDropdown();
+ onSelectBlock(blockId);
+ }}
+ />}
+
+
+
);
}
@@ -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,
};
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx
index 1c468b4ca009..85fb8c72d605 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx
@@ -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(
- ,
+
+
+ ,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
@@ -27,13 +36,18 @@ describe('ProblemBrowser Main component', () => {
test('render with selected block', () => {
const component = renderer.create(
- ,
+
+
+ ,
);
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(
- ,
+
+
+ ,
);
- 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(
,
);
- 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();
});
});
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/MainContainer.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/MainContainer.jsx
index a36b31978be9..55c40facf299 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/MainContainer.jsx
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/MainContainer.jsx
@@ -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(
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap
index 2fafa3a6a3d5..115d58389259 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap
@@ -2,44 +2,80 @@
exports[`ProblemBrowser Main component render with basic parameters 1`] = `
-
-
+ Select a section or problem
+
+
+
+
+
`;
exports[`ProblemBrowser Main component render with selected block 1`] = `
-
-
+ Select a section or problem
+
+
+
+
+
`;
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatus.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatus.jsx
new file mode 100644
index 000000000000..9715493aa914
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatus.jsx
@@ -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 = (
+
+ {gettext('Your report is being generated...')}
+
+
+ );
+
+ const successMessage = (
+
+ );
+
+ const errorMessage = (
+
+ {error && `${gettext('Error')}: `}
+ {error}
+
+ );
+
+ return (
+
+ {inProgress && progressMessage}
+ {error && errorMessage}
+ {succeeded && successMessage}
+
+ );
+};
+
+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;
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatus.test.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatus.test.jsx
new file mode 100644
index 000000000000..7c163f15e50a
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatus.test.jsx
@@ -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(
+ ,
+ );
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('render success status', () => {
+ const component = renderer.create(
+ ,
+ );
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('render error status', () => {
+ const component = renderer.create(
+ ,
+ );
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatusContainer.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatusContainer.jsx
new file mode 100644
index 000000000000..664cc46bda91
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/ReportStatusContainer.jsx
@@ -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;
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/__snapshots__/ReportStatus.test.jsx.snap b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/__snapshots__/ReportStatus.test.jsx.snap
new file mode 100644
index 000000000000..a3c052ff7ac6
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/ReportStatus/__snapshots__/ReportStatus.test.jsx.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReportStatus component render error status 1`] = `
+
+
+ Error:
+ some error status
+
+
+`;
+
+exports[`ReportStatus component render in progress status 1`] = `
+
+
+ Your report is being generated...
+
+
+
+
+
+`;
+
+exports[`ReportStatus component render success status 1`] = `
+
+`;
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/actions/constants.js b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/actions/constants.js
new file mode 100644
index 000000000000..e643833285be
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/actions/constants.js
@@ -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';
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/actions/problemResponses.js b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/actions/problemResponses.js
new file mode 100644
index 000000000000..a129340767dd
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/actions/problemResponses.js
@@ -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,
+};
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/api/client.js b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/api/client.js
new file mode 100644
index 000000000000..532af508a5b9
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/api/client.js
@@ -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,
+};
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/reducers/index.js b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/reducers/index.js
new file mode 100644
index 000000000000..0c0c6a8c270c
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/reducers/index.js
@@ -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,
+});
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/store.js b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/store.js
new file mode 100644
index 000000000000..dd9314f9755c
--- /dev/null
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/data/store.js
@@ -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;
diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx
index 2e8b7849bb77..2ecf41bab61d 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx
@@ -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 => (
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index cb76c48e9d05..3741e16f1634 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -2564,7 +2564,7 @@ def test_get_problem_responses_invalid_location(self):
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 @@ def test_list_report_downloads(self):
@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:
diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py
index 7beead617f07..8491e9a2dd68 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/grades.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py
@@ -973,7 +973,11 @@ def generate(cls, _xmodule_instance_args, _entry_id, course_id, task_input, acti
# 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)
diff --git a/lms/djangoapps/instructor_task/tasks_helper/utils.py b/lms/djangoapps/instructor_task/tasks_helper/utils.py
index b5552029adac..cb72ea4877f5 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/utils.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/utils.py
@@ -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):
diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss
index f6e10574f386..1a319abf9f65 100644
--- a/lms/static/sass/course/instructor/_instructor_2.scss
+++ b/lms/static/sass/course/instructor/_instructor_2.scss
@@ -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;
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html
index a06d7160b267..051bb48ad179 100644
--- a/lms/templates/instructor/instructor_dashboard_2/data_download.html
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html
@@ -58,22 +58,18 @@ ${_("Reports")}
%endif
-
-
+
${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"
}
)}
-
-
-
-
-
-
+
${_("Click to list certificates that are issued for this course:")}