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 ( -
-
+ ); } @@ -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 = ( +
+ {gettext('Your report has being successfully generated.')} + + +
+ ); + + 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`] = ` +
+
+ Your report has being successfully generated. + +
+ +
+ View Report +
+
+
+`; 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:")}