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 a9c9c5c61d..c919438665 100644
--- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx
+++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx
@@ -1,15 +1,13 @@
/* global gettext */
-import { Button } from '@edx/paragon';
-import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer';
+import { Button, Icon } from '@edx/paragon';
+import { BlockBrowser } from 'BlockBrowser';
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,
};
@@ -24,39 +22,31 @@ 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);
- }}
- />}
-
-
-
+
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
+
+ {selectedBlock || 'Select a section or problem'}
+
+
+
+
+
+
+ {this.state.showDropdown &&
+ {
+ this.hideDropdown();
+ onSelectBlock(blockId);
+ }}
+ />}
);
}
@@ -64,17 +54,13 @@ 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: '',
- timeout: null,
+ selectedBlock: 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 85fb8c72d6..c25973868b 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,34 +1,25 @@
/* global jest,test,describe,expect */
import { Button } from '@edx/paragon';
-import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer';
-import { Provider } from 'react-redux';
+import { BlockBrowser } from 'BlockBrowser';
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();
@@ -36,18 +27,13 @@ describe('ProblemBrowser Main component', () => {
test('render with selected block', () => {
const component = renderer.create(
-
-
- ,
+
,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
@@ -56,20 +42,15 @@ describe('ProblemBrowser Main component', () => {
test('fetch course block on toggling dropdown', () => {
const fetchCourseBlocksMock = jest.fn();
const component = renderer.create(
-
-
- ,
+
,
);
- const instance = component.root.children[0].instance;
+ const instance = component.getInstance();
instance.handleToggleDropdown();
expect(fetchCourseBlocksMock.mock.calls.length).toBe(1);
});
@@ -78,17 +59,13 @@ describe('ProblemBrowser Main component', () => {
const component = shallow(
,
);
- expect(component.find(BlockBrowserContainer).length).toBeFalsy();
- component.find(Button).find({ label: 'Select a section or problem' }).simulate('click');
- expect(component.find(BlockBrowserContainer).length).toBeTruthy();
+ component.find('.problem-selector').simulate('click');
+ expect(component.find(BlockBrowser)).toBeTruthy();
});
});
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 115d583892..c6bbea41e6 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,80 +2,90 @@
exports[`ProblemBrowser Main component render with basic parameters 1`] = `
`;
exports[`ProblemBrowser Main component render with selected block 1`] = `
`;
diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
index 3a140a057b..b221f8b734 100644
--- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
+++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
@@ -26,6 +26,7 @@ from lms.djangoapps.courseware.tests.factories import StaffFactory, StudentModul
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags
from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info
+from lms.djangoapps.instructor.toggles import DATA_DOWNLOAD_V2
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from student.models import CourseEnrollment
@@ -136,31 +137,39 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT
self.assertTrue(has_instructor_tab(org_researcher, self.course))
@ddt.data(
- ('staff', False),
- ('instructor', False),
- ('data_researcher', True),
- ('global_staff', True),
+ ('staff', False, False),
+ ('instructor', False, False),
+ ('data_researcher', True, False),
+ ('global_staff', True, False),
+ ('staff', False, True),
+ ('instructor', False, True),
+ ('data_researcher', True, True),
+ ('global_staff', True, True),
)
@ddt.unpack
- def test_data_download(self, access_role, can_access):
+ def test_data_download(self, access_role, can_access, waffle_status):
"""
Verify that the Data Download tab only shows up for certain roles
"""
- download_section = '
Data Download '
- user = UserFactory.create(is_staff=access_role == 'global_staff')
- CourseAccessRoleFactory(
- course_id=self.course.id,
- user=user,
- role=access_role,
- org=self.course.id.org
- )
- self.client.login(username=user.username, password="test")
- response = self.client.get(self.url)
- if can_access:
- self.assertContains(response, download_section)
- else:
- self.assertNotContains(response, download_section)
+ with override_waffle_flag(DATA_DOWNLOAD_V2, waffle_status):
+ download_section = '
Data Download '
+ if waffle_status:
+ download_section = '
Data Download '
+ user = UserFactory.create(is_staff=access_role == 'global_staff')
+ CourseAccessRoleFactory(
+ course_id=self.course.id,
+ user=user,
+ role=access_role,
+ org=self.course.id.org
+ )
+ self.client.login(username=user.username, password="test")
+ response = self.client.get(self.url)
+ if can_access:
+ self.assertContains(response, download_section)
+ else:
+ self.assertNotContains(response, download_section)
@override_settings(ANALYTICS_DASHBOARD_URL='http://example.com')
@override_settings(ANALYTICS_DASHBOARD_NAME='Example')
diff --git a/lms/djangoapps/instructor/toggles.py b/lms/djangoapps/instructor/toggles.py
new file mode 100644
index 0000000000..3e4e58a423
--- /dev/null
+++ b/lms/djangoapps/instructor/toggles.py
@@ -0,0 +1,54 @@
+"""
+Waffle flags for instructor dashboard.
+"""
+
+from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleFlag
+
+WAFFLE_NAMESPACE = 'instructor'
+# Namespace for instructor waffle flags.
+WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=WAFFLE_NAMESPACE)
+
+# Waffle flag enable new data download UI on specific course.
+# .. toggle_name: instructor.enable_data_download_v2
+# .. toggle_implementation: WaffleFlag
+# .. toggle_default: False
+# .. toggle_description: instructor
+# .. toggle_category: Instructor dashboard
+# .. toggle_use_cases: incremental_release, open_edx
+# .. toggle_creation_date: 2020-07-8
+# .. toggle_expiration_date: ??
+# .. toggle_warnings: ??
+# .. toggle_tickets: PROD-1309
+# .. toggle_status: supported
+DATA_DOWNLOAD_V2 = CourseWaffleFlag(
+ waffle_namespace=WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='instructor_dashboard: '),
+ flag_name='enable_data_download_v2',
+)
+
+# Waffle flag to use optimised is_small_course.
+# .. toggle_name: verify_student.optimised_is_small_course
+# .. toggle_implementation: WaffleFlag
+# .. toggle_default: False
+# .. toggle_description: Supports staged rollout to improved is_small_course method.
+# .. toggle_category: instructor
+# .. toggle_use_cases: incremental_release, open_edx
+# .. toggle_creation_date: 2020-07-02
+# .. toggle_expiration_date: n/a
+# .. toggle_warnings: n/a
+# .. toggle_tickets: PROD-1740
+# .. toggle_status: supported
+OPTIMISED_IS_SMALL_COURSE = WaffleFlag(
+ waffle_namespace=WAFFLE_FLAG_NAMESPACE,
+ flag_name='optimised_is_small_course',
+)
+
+
+def data_download_v2_is_enabled(course_key):
+ """
+ check if data download v2 is enabled.
+ """
+ return DATA_DOWNLOAD_V2.is_enabled(course_key)
+
+
+def use_optimised_is_small_course():
+ return OPTIMISED_IS_SMALL_COURSE.is_enabled()
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index 7b39156a8a..ac587233f3 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -65,6 +65,7 @@ from xmodule.tabs import CourseTab
from .tools import get_units_with_due_date, title_or_url
from .. import permissions
+from ..toggles import data_download_v2_is_enabled
log = logging.getLogger(__name__)
@@ -600,9 +601,9 @@ def _section_data_download(course, access):
settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
course.enable_proctored_exams
)
-
+ section_key = 'data_download_2' if data_download_v2_is_enabled(course_key) else 'data_download'
section_data = {
- 'section_key': 'data_download',
+ 'section_key': section_key,
'section_display_name': _('Data Download'),
'access': access,
'show_generate_proctored_exam_report_button': show_proctored_report_button,
diff --git a/lms/static/js/fixtures/instructor_dashboard/data_download.html b/lms/static/js/fixtures/instructor_dashboard/data_download.html
index ac50e51355..500ba6d31a 100644
--- a/lms/static/js/fixtures/instructor_dashboard/data_download.html
+++ b/lms/static/js/fixtures/instructor_dashboard/data_download.html
@@ -1,9 +1,224 @@
-
-
${_("Click to list certificates that are issued for this course:")}
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+ Reports
+
+
+ Problem Report
+
+
+ Certificates
+
+
+ Grading
+
+
+
+
+
+
+
+
+
+
+ Grading Configuration
+
+ Anonymized Student IDs
+
+ Profile Information
+
+
+ Learner
+ who can enroll
+
+
+ List enrolled students profile information
+
+ Proctored exam results
+
+
+ Survey Result report
+
+ ORA Data
+ report
+
+ Problem Grade report
+
+
+ Download
+ report
+
+
+
+
+
+
+
${_("Click to display the grading configuration for the \
+ course. The grading configuration is the breakdown of graded subsections of the course \
+ (such as exams and problem sets), and can be changed on the 'Grading' \
+ page (under 'Settings') in Studio.")}
+
${_("Click to download a CSV of \
+ anonymized student IDs:")}
+
+
${_("For large courses, generating some reports can take \
+ several hours. When report generation is complete, a \
+ link that includes the date and time of generation appears in the table below. These reports are \
+ generated in the background, meaning it is OK to navigate away from this page while your report is \
+ generating.")}
+
+
${_("Please be patient and do not click these buttons \
+ multiple times. Clicking these buttons multiple times will significantly slow the generation \
+ process.")}
+
+
${_("For smaller courses, click to list \
+ profile information for enrolled students directly on this page:")}
+
${_("Click to generate a CSV file of \
+ all students enrolled in this course, along with profile information such as email address and \
+ username:")}
+
+
${_("Click to generate a CSV file \
+ that lists learners who can enroll in the course but have not yet done so.")}
+
+
${_("Click to generate a CSV file \
+ of all proctored exam results in this course.")}
+
+
${_("Click to generate a CSV file of \
+ survey results for this course.")}
+
+
+
+
+
+
+
+ View certificates
+
+ Download csv of
+ certificates
+
+
+ Download
+ report
+
+
+
${_("Click to list certificates that are issued for this course:")}
+
+
+
+
+
+ ${static.renderReact(
+ component="ProblemBrowser",
+ id="react-block-listing",
+ props={
+ "courseId": course.id,
+ "excludeBlockTypes": ['html', 'video', 'discussion']
+ }
+ )}
+
+ Download
+ report
+
+
+ ${_("NOTE")}:
+ ${_("The generated report is limited to {max_entries} responses. If you expect more than {max_entries} "
+ "responses, try generating the report on a chapter-by-chapter, or problem-by-problem basis, or contact "
+ "your site administrator to increase the limit.").format(max_entries=max_entries)}
+
+
+
+
+
+
+ Learner status
+
+ Verified Learners Only
+ All Learners
+
+
+ Download Report
+
+
+
${_("Click to generate a CSV grade report for all currently enrolled students.")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
${_("Reports Available for Download")}
+
+ ${_("The reports listed below are available for download, identified by UTC date and time of generation.")}
+
+
+
+ ${_("The answer distribution report listed below is generated periodically by an automated background process. \
+ The report is cumulative, so answers submitted after the process starts are included in a subsequent report. \
+ The report is generated several times per day.")}
+
+
+ ${Text(_("{strong_start}Note{strong_end}: {ul_start}{li_start}To keep student data secure, you cannot save or \
+ email these links for direct access. Copies of links expire within 5 minutes.{li_end}{li_start}Report files \
+ are deleted 90 days after generation. If you will need access to old reports, download and store the files, \
+ in accordance with your institution's data security policies.{li_end}{ul_end}")).format(
+ strong_start=HTML(""),
+ strong_end=HTML(" "),
+ ul_start=HTML("
"),
+ li_start=HTML("
"),
+ li_end=HTML(" "),
+ )}
+
+
+
+
+
+
+
+
${_("Pending Tasks")}
+
+
${_("The status for any active tasks appears in a table below.")}
+
+
+
+
+
+
diff --git a/lms/static/js/instructor_dashboard/data_download_2.js b/lms/static/js/instructor_dashboard/data_download_2.js
new file mode 100644
index 0000000000..8416f090ba
--- /dev/null
+++ b/lms/static/js/instructor_dashboard/data_download_2.js
@@ -0,0 +1,257 @@
+/* globals _, DataDownloadV2, PendingInstructorTasks, ReportDownloads */
+
+(function() {
+ 'use strict';
+ // eslint-disable-next-line no-unused-vars
+ var DataDownloadV2, PendingInstructorTasks, ReportDownloads, statusAjaxError;
+
+ statusAjaxError = function() {
+ return window.InstructorDashboard.util.statusAjaxError.apply(this, arguments);
+ };
+
+ PendingInstructorTasks = function() {
+ return window.InstructorDashboard.util.PendingInstructorTasks;
+ };
+
+ ReportDownloads = function() {
+ return window.InstructorDashboard.util.ReportDownloads;
+ };
+
+ DataDownloadV2 = (function() {
+ function InstructorDashboardDataDownload($section) {
+ var dataDownloadObj = this;
+ this.$section = $section;
+ this.$section.data('wrapper', this);
+ this.$list_problem_responses_csv_input = this.$section.find("input[name='problem-location']");
+ this.$download_display_text = $('.data-display-text');
+ this.$download_request_response_error = $('.request-response-error');
+ this.$download_display_table = $('.profile-data-display-table');
+ this.$reports_request_response = $('.request-response');
+ this.$reports_request_response_error = $('.request-response-error');
+ this.report_downloads = new (ReportDownloads())(this.$section);
+ this.instructor_tasks = new (PendingInstructorTasks())(this.$section);
+ this.$download_report = $('.download-report');
+ this.$gradeReportDownload = $('.grade-report-download');
+ this.$report_type_selector = $('.report-type');
+ this.$selection_informations = $('.selectionInfo');
+ this.$data_display_table = $('.data-display-table-holder');
+ this.$downloadProblemReport = $('#download-problem-report');
+ this.$tabSwitch = $('.data-download-nav .btn-link');
+ this.$selectedSection = $('#' + this.$tabSwitch.first().attr('data-section'));
+ this.$learnerStatus = $('.learner-status');
+
+ this.ERROR_MESSAGES = {
+ ORADataReport: gettext('Error generating ORA data report. Please try again.'),
+ problemGradeReport: gettext('Error generating problem grade report. Please try again.'),
+ profileInformation: gettext('Error generating student profile information. Please try again.'),
+ surveyResultReport: gettext('Error generating survey results. Please try again.'),
+ proctoredExamResults: gettext('Error generating proctored exam results. Please try again.'),
+ learnerWhoCanEnroll: gettext('Error generating list of students who may enroll. Please try again.'),
+ viewCertificates: gettext('Error getting issued certificates list.')
+ };
+
+ /**
+ * Removes text error/success messages and tables from UI
+ */
+ this.clear_display = function() {
+ this.$download_display_text.empty();
+ this.$download_display_table.empty();
+ this.$download_request_response_error.empty();
+ this.$reports_request_response.empty();
+ this.$reports_request_response_error.empty();
+ this.$data_display_table.empty();
+ $('.msg-confirm').css({
+ display: 'none'
+ });
+ return $('.msg-error').css({
+ display: 'none'
+ });
+ };
+
+ this.clear_display();
+
+ /**
+ * Show and hide selected tab data
+ */
+ this.$tabSwitch.click(function(event) {
+ var selectedSection = '#' + $(this).attr('data-section');
+ event.preventDefault();
+ $('.data-download-nav .btn-link').removeClass('active-section');
+ $('section.tab-data').hide();
+ $(selectedSection).show();
+ $(this).addClass('active-section');
+
+ $(this).find('select').trigger('change');
+ dataDownloadObj.$selectedSection = $(selectedSection);
+
+ dataDownloadObj.clear_display();
+ });
+
+ this.$tabSwitch.first().click();
+
+ /**
+ * on change of report select update show and hide related descriptions
+ */
+ this.$report_type_selector.change(function() {
+ var selectedOption = dataDownloadObj.$report_type_selector.val();
+ dataDownloadObj.$selection_informations.each(function(index, elem) {
+ if ($(elem).hasClass(selectedOption)) {
+ $(elem).show();
+ } else {
+ $(elem).hide();
+ }
+ });
+ dataDownloadObj.clear_display();
+ });
+
+ this.selectedOption = function() {
+ return dataDownloadObj.$selectedSection.find('select').find('option:selected');
+ };
+
+ /**
+ * On click download button get selected option and pass it to handler function.
+ */
+ this.downloadReportClickHandler = function() {
+ var selectedOption = dataDownloadObj.selectedOption();
+ var errorMessage = dataDownloadObj.ERROR_MESSAGES[selectedOption.val()];
+
+ if (selectedOption.data('directdownload')) {
+ location.href = selectedOption.data('endpoint') + '?csv=true';
+ } else if (selectedOption.data('datatable')) {
+ dataDownloadObj.renderDataTable(selectedOption);
+ } else {
+ dataDownloadObj.downloadCSV(selectedOption, errorMessage, false);
+ }
+ };
+ this.$download_report.click(dataDownloadObj.downloadReportClickHandler);
+
+ /**
+ * Call data endpoint and invoke buildDataTable to render Table UI.
+ * @param selected option element from report selector to get data-endpoint.
+ * @param errorMessage Error message in case endpoint call fail.
+ */
+ this.renderDataTable = function(selected, errorMessage) {
+ var url = selected.data('endpoint');
+ dataDownloadObj.clear_display();
+ dataDownloadObj.$data_display_table.text(gettext('Loading data...'));
+ return $.ajax({
+ type: 'POST',
+ url: url,
+ error: function(error) {
+ dataDownloadObj.OnError(error, errorMessage);
+ },
+ success: function(data) {
+ dataDownloadObj.buildDataTable(data);
+ }
+ });
+ };
+
+
+ this.$downloadProblemReport.click(function() {
+ var data = {problem_location: dataDownloadObj.$list_problem_responses_csv_input.val()};
+ dataDownloadObj.downloadCSV($(this), false, data);
+ });
+
+ this.$gradeReportDownload.click(function() {
+ var errorMessage = gettext('Error generating grades. Please try again.');
+ var data = {verified_learners_only: dataDownloadObj.$learnerStatus.val()};
+ dataDownloadObj.downloadCSV($(this), errorMessage, data);
+ });
+
+ /**
+ * Call data endpoint and render success/error message on dashboard UI.
+ */
+ this.downloadCSV = function(selected, errorMessage, postData) {
+ var url = selected.data('endpoint');
+ dataDownloadObj.clear_display();
+ return $.ajax({
+ type: 'POST',
+ dataType: 'json',
+ url: url,
+ data: postData,
+ error: function(error) {
+ dataDownloadObj.OnError(error, errorMessage);
+ },
+ success: function(data) {
+ if (data.grading_config_summary) {
+ edx.HtmlUtils.setHtml(
+ dataDownloadObj.$download_display_text, edx.HtmlUtils.HTML(data.grading_config_summary));
+ } else {
+ dataDownloadObj.$reports_request_response.text(data.status);
+ $('.msg-confirm').css({display: 'block'});
+ }
+ }
+ });
+ };
+
+ this.OnError = function(error, errorMessage) {
+ dataDownloadObj.clear_display();
+ if (error.responseText) {
+ // eslint-disable-next-line no-param-reassign
+ errorMessage = JSON.parse(error.responseText);
+ }
+ dataDownloadObj.$download_request_response_error.text(errorMessage);
+ return dataDownloadObj.$download_request_response_error.css({
+ display: 'block'
+ });
+ };
+ /**
+ * render data table on dashboard UI with given data.
+ */
+ this.buildDataTable = function(data) {
+ var $tablePlaceholder, columns, feature, gridData, options;
+ dataDownloadObj.clear_display();
+ options = {
+ enableCellNavigation: true,
+ enableColumnReorder: false,
+ forceFitColumns: true,
+ rowHeight: 35
+ };
+ columns = (function() {
+ var i, len, ref, results;
+ ref = data.queried_features;
+ results = [];
+ for (i = 0, len = ref.length; i < len; i++) {
+ feature = ref[i];
+ results.push({
+ id: feature,
+ field: feature,
+ name: data.feature_names[feature]
+ });
+ }
+ return results;
+ }());
+ gridData = data.hasOwnProperty('students') ? data.students : data.certificates;
+ $tablePlaceholder = $('
', {
+ class: 'slickgrid'
+ });
+ dataDownloadObj.$download_display_table.append($tablePlaceholder);
+ return new window.Slick.Grid($tablePlaceholder, gridData, columns, options);
+ };
+ }
+
+ InstructorDashboardDataDownload.prototype.onClickTitle = function() {
+ this.clear_display();
+ this.instructor_tasks.task_poller.start();
+ return this.report_downloads.downloads_poller.start();
+ };
+
+ InstructorDashboardDataDownload.prototype.onExit = function() {
+ this.instructor_tasks.task_poller.stop();
+ return this.report_downloads.downloads_poller.stop();
+ };
+ return InstructorDashboardDataDownload;
+ }());
+
+ _.defaults(window, {
+ InstructorDashboard: {}
+ });
+
+ _.defaults(window.InstructorDashboard, {
+ sections: {}
+ });
+
+ _.defaults(window.InstructorDashboard.sections, {
+ DataDownloadV2: DataDownloadV2
+ });
+}).call(this);
diff --git a/lms/static/js/instructor_dashboard/instructor_dashboard.js b/lms/static/js/instructor_dashboard/instructor_dashboard.js
index c58e7610e9..b617bb8144 100644
--- a/lms/static/js/instructor_dashboard/instructor_dashboard.js
+++ b/lms/static/js/instructor_dashboard/instructor_dashboard.js
@@ -163,6 +163,9 @@ such that the value can be defined later than this assignment (file load order).
}, {
constructor: window.InstructorDashboard.sections.DataDownload,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download')
+ }, {
+ constructor: window.InstructorDashboard.sections.DataDownloadV2,
+ $element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download_2')
}, {
constructor: window.InstructorDashboard.sections.ECommerce,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#e-commerce')
diff --git a/lms/static/js/spec/instructor_dashboard/data_download_spec.js b/lms/static/js/spec/instructor_dashboard/data_download_spec.js
index 0e52e2c9b0..ad4c2bcbbf 100644
--- a/lms/static/js/spec/instructor_dashboard/data_download_spec.js
+++ b/lms/static/js/spec/instructor_dashboard/data_download_spec.js
@@ -1,70 +1,162 @@
-/* global define */
-define(['jquery',
- 'js/instructor_dashboard/data_download',
- 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
- 'slick.grid'],
- function($, DataDownload, AjaxHelpers) {
- 'use strict';
- describe('edx.instructor_dashboard.data_download.DataDownload_Certificate', function() {
- var url, data_download_certificate;
+/* global define, DataDownload */
- beforeEach(function() {
- loadFixtures('js/fixtures/instructor_dashboard/data_download.html');
- data_download_certificate = new window.DataDownload_Certificate($('.issued_certificates'));
- url = '/courses/PU/FSc/2014_T4/instructor/api/get_issued_certificates';
- data_download_certificate.$list_issued_certificate_table_btn.data('endpoint', url);
- });
+define([
+ 'jquery',
+ 'js/instructor_dashboard/data_download_2',
+ 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
+],
+ function($, id, AjaxHelper) {
+ 'use strict';
+ describe('edx.instructor_dashboard.data_download', function() {
+ var requests, $selected, dataDownload, url, errorMessage;
- it('show data on success callback', function() {
- // Spy on AJAX requests
- var requests = AjaxHelpers.requests(this);
- var data = {
- certificates: [{course_id: 'xyz_test', mode: 'honor'}],
- queried_features: ['course_id', 'mode'],
- feature_names: {course_id: 'Course ID', mode: ' Mode'}
- };
+ beforeEach(function() {
+ loadFixtures('js/fixtures/instructor_dashboard/data_download.html');
- data_download_certificate.$list_issued_certificate_table_btn.click();
- AjaxHelpers.expectJsonRequest(requests, 'POST', url);
+ dataDownload = window.InstructorDashboard.sections;
+ dataDownload.DataDownloadV2($('#data_download_2'));
+ window.InstructorDashboard.util.PendingInstructorTasks = function() {
+ return;
+ };
+ requests = AjaxHelper.requests(this);
+ $selected = $('
');
+ url = $selected.data('endpoint');
+ errorMessage = 'An Error is occurred with request';
+ });
- // Simulate a success response from the server
- AjaxHelpers.respondWithJson(requests, data);
- expect(data_download_certificate.$certificate_display_table.html()
- .indexOf('Course ID') !== -1).toBe(true);
- expect(data_download_certificate.$certificate_display_table.html()
- .indexOf('Mode') !== -1).toBe(true);
- expect(data_download_certificate.$certificate_display_table.html()
- .indexOf('xyz_test') !== -1).toBe(true);
- expect(data_download_certificate.$certificate_display_table.html()
- .indexOf('honor') !== -1).toBe(true);
- });
+ it('renders success message properly', function() {
+ dataDownload.downloadCSV($selected, errorMessage);
- it('show error on failure callback', function() {
- // Spy on AJAX requests
- var requests = AjaxHelpers.requests(this);
+ AjaxHelper.expectRequest(requests, 'POST', url);
+ AjaxHelper.respondWithJson(requests, {
+ status: 'Request is succeeded'
+ });
+ expect(dataDownload.$reports_request_response.text()).toContain('Request is succeeded');
+ });
- data_download_certificate.$list_issued_certificate_table_btn.click();
- // Simulate a error response from the server
- AjaxHelpers.respondWithError(requests);
- expect(data_download_certificate.$certificates_request_response_error.text())
- .toEqual('Error getting issued certificates list.');
- });
- it('error should be clear from UI on success callback', function() {
- var requests = AjaxHelpers.requests(this);
- data_download_certificate.$list_issued_certificate_table_btn.click();
+ it('renders grading config returned by the server in case of successful request ', function() {
+ dataDownload.downloadCSV($selected, errorMessage);
- // Simulate a error response from the server
- AjaxHelpers.respondWithError(requests);
- expect(data_download_certificate.$certificates_request_response_error.text())
- .toEqual('Error getting issued certificates list.');
+ AjaxHelper.expectRequest(requests, 'POST', url);
+ AjaxHelper.respondWithJson(requests, {
+ grading_config_summary: 'This is grading config'
+ });
+ expect(dataDownload.$download_display_text.text()).toContain('This is grading config');
+ });
- // Simulate a success response from the server
- data_download_certificate.$list_issued_certificate_table_btn.click();
- AjaxHelpers.expectJsonRequest(requests, 'POST', url);
+ it('renders enrolled student list in case of successful request ', function() {
+ var data = {
+ available_features: [
+ 'id',
+ 'username',
+ 'first_name',
+ 'last_name',
+ 'is_staff',
+ 'email',
+ 'date_joined',
+ 'last_login',
+ 'name',
+ 'language',
+ 'location',
+ 'year_of_birth',
+ 'gender',
+ 'level_of_education',
+ 'mailing_address',
+ 'goals',
+ 'meta',
+ 'city',
+ 'country'
+ ],
+ course_id: 'test_course_101',
+ feature_names: {
+ gender: 'Gender',
+ goals: 'Goals',
+ enrollment_mode: 'Enrollment Mode',
+ email: 'Email',
+ country: 'Country',
+ id: 'User ID',
+ mailing_address: 'Mailing Address',
+ last_login: 'Last Login',
+ date_joined: 'Date Joined',
+ location: 'Location',
+ city: 'City',
+ verification_status: 'Verification Status',
+ year_of_birth: 'Birth Year',
+ name: 'Name',
+ username: 'Username',
+ level_of_education: 'Level of Education',
+ language: 'Language'
+ },
+ students: [
+ {
+ gender: 'Male',
+ goals: 'Goal',
+ enrollment_mode: 'audit',
+ email: 'test@example.com',
+ country: 'PK',
+ year_of_birth: 'None',
+ id: '8',
+ mailing_address: 'None',
+ last_login: '2020-06-17T08:17:00.561Z',
+ date_joined: '2019-09-25T20:06:17.564Z',
+ location: 'None',
+ verification_status: 'N/A',
+ city: 'None',
+ name: 'None',
+ username: 'test',
+ level_of_education: 'None',
+ language: 'None'
+ }
+ ],
+ queried_features: [
+ 'id',
+ 'username',
+ 'name',
+ 'email',
+ 'language',
+ 'location',
+ 'year_of_birth',
+ 'gender',
+ 'level_of_education',
+ 'mailing_address',
+ 'goals',
+ 'enrollment_mode',
+ 'verification_status',
+ 'last_login',
+ 'date_joined',
+ 'city',
+ 'country'
+ ],
+ students_count: 1
+ };
+ dataDownload.renderDataTable($selected, errorMessage);
+ AjaxHelper.expectRequest(requests, 'POST', url);
+ AjaxHelper.respondWithJson(requests, data);
+ // eslint-disable-next-line vars-on-top
+ var dataTable = dataDownload.$data_display_table.html();
+ // eslint-disable-next-line vars-on-top
+ var existInHtml = function(value) {
+ expect(dataTable.indexOf(data.feature_names[value]) !== -1).toBe(false);
+ expect(dataTable.indexOf(data.students[0][value]) !== -1).toBe(false);
+ };
+ data.queried_features.forEach(existInHtml);
+ });
- expect(data_download_certificate.$certificates_request_response_error.text())
- .not.toEqual('Error getting issued certificates list');
- });
- });
- });
+
+ it('calls renderDataTable function if data-datatable is true', function() {
+ $selected = $selected.attr('data-datatable', true);
+ spyOn(dataDownload, 'selectedOption').and.returnValue($selected);
+ spyOn(dataDownload, 'renderDataTable');
+ dataDownload.downloadReportClickHandler();
+ expect(dataDownload.renderDataTable).toHaveBeenCalled();
+ });
+
+ it('calls downloadCSV function if no other data type is specified', function() {
+ spyOn(dataDownload, 'selectedOption').and.returnValue($selected);
+ spyOn(dataDownload, 'downloadCSV');
+ dataDownload.downloadReportClickHandler();
+ expect(dataDownload.downloadCSV).toHaveBeenCalled();
+ });
+ });
+ });
diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js
index 7c0cdb2c12..cb93e8bb0c 100644
--- a/lms/static/lms/js/spec/main.js
+++ b/lms/static/lms/js/spec/main.js
@@ -757,6 +757,7 @@
'js/spec/financial-assistance/financial_assistance_form_view_spec.js',
'js/spec/groups/views/cohorts_spec.js',
'js/spec/groups/views/discussions_spec.js',
+ 'js/spec/instructor_dashboard/data_download_spec.js',
'js/spec/instructor_dashboard/certificates_bulk_exception_spec.js',
'js/spec/instructor_dashboard/certificates_exception_spec.js',
'js/spec/instructor_dashboard/certificates_invalidation_spec.js',
diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss
index 1a319abf9f..8c509acaae 100644
--- a/lms/static/sass/course/instructor/_instructor_2.scss
+++ b/lms/static/sass/course/instructor/_instructor_2.scss
@@ -313,28 +313,9 @@
}
}
}
-
- .report-generation-status {
- .msg {
- display: inherit;
-
- &.error {
- color: $error-color;
- }
-
- > div {
- display: inline-block;
- }
-
- a {
- margin: 0 1rem;
-
- & > div {
- display: inline-block;
- }
- }
+ .data-download-nav {
+ @extend .instructor-nav
}
- }
}
// elements - general
@@ -1506,8 +1487,33 @@
// view - data download
// --------------------
-.instructor-dashboard-wrapper-2 section.idash-section#data_download {
- input {
+
+
+.instructor-dashboard-wrapper-2 section.idash-section#data_download_2 {
+ .data-download-grid-container {
+ display:grid;
+ grid-template-columns:repeat(auto-fit, minmax(20rem, 1fr));
+ grid-auto-rows: minmax(250px, auto);
+ div.card {
+ border-right: 2px solid grey;
+ border-bottom: 2px solid grey;
+ padding: 1em;
+ display: grid;
+ grid-template-rows: 1fr 3fr;
+ p.grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ align-self: self-end;
+ }
+ .problem-browser {
+ display: grid;
+ grid-gap: 1em;
+ }
+ }
+ }
+
+
+ input {
margin-bottom: 1em;
line-height: 1.3em;
}
@@ -1531,8 +1537,79 @@
}
}
- #react-problem-report {
- margin: $baseline 0;
+ .block-browser {
+ .header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ .title {
+ margin: 0 0.5em;
+ }
+ }
+
+ ul {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ margin: 0;
+
+ li {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin: 0.25em 0;
+
+ .block-name {
+ flex-grow: 1;
+ margin-right: 0.5em;
+ text-align: left;
+ }
+ }
+ }
+ }
+
+ .problem-browser {
+ .block-browser {
+ position: absolute;
+ background: white;
+ padding: 5px;
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
+ z-index: 2;
+ }
+
+ input {
+ max-width: 800px;
+ width: 100%;
+ margin-bottom: 0;
+ }
+ }
+
+}
+
+.instructor-dashboard-wrapper-2 section.idash-section#data_download {
+ input {
+ margin-bottom: 1em;
+ line-height: 1.3em;
+ }
+
+ .reports-download-container {
+ .data-display-table {
+ .slickgrid {
+ height: 400px;
+ }
+ }
+
+ .report-downloads-table {
+ .slickgrid {
+ height: 300px;
+ padding: ($baseline/4);
+ }
+ // Disable horizontal scroll bar when grid only has 1 column. Remove this CSS class when more columns added.
+ .slick-viewport {
+ overflow-x: hidden !important;
+ }
+ }
}
.block-browser {
@@ -2970,3 +3047,41 @@ div.staff_actions {
color: theme-color("success");
}
}
+
+.action-type-container p {
+ line-height: 2;
+}
+
+.mb-15 {
+ margin-bottom: 20px;
+}
+.mb-20 {
+ margin-bottom: 20px;
+}
+
+.download-report {
+ margin-left: 10px;
+}
+
+.selector {
+ width: 315px;
+ height: 34px
+}
+.data-download-container .message {
+ border-radius: 1px;
+ padding: 10px 15px;
+ margin-bottom: 20px;
+ font-weight: 600;
+}
+.font-size-100 {
+ font-size: 100%
+}
+
+.problem-selector{
+ height: 34px;
+ min-width: 300px;
+ max-width: 780px;
+ border: 1px solid grey;
+ display: block;
+ padding: 8px;
+}
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2.html
new file mode 100644
index 0000000000..fe3e12cece
--- /dev/null
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2.html
@@ -0,0 +1,93 @@
+<%page args="section_data" expression_filter="h"/>
+<%namespace name='static' file='/static_content.html'/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+
+
+
+
+ Reports
+
+ %if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'):
+
+ Problem Report
+
+
+ Certificates
+
+ %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
+
+ Grading
+
+ %endif
+ %endif
+
+
+ <%include file="./data_download_2/reports.html" args="section_data=section_data, **context.kwargs" />
+
+ <%include file="./data_download_2/grading.html" args="section_data=section_data, **context.kwargs" />
+
+ %if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'):
+ <%include file="./data_download_2/certificates.html" args="section_data=section_data, **context.kwargs" />
+ <%include file="./data_download_2/problem_report.html" args="section_data=section_data, **context.kwargs" />
+ %endif
+
+
+
+
+
+
+
+
+
+
+
+
${_("Reports Available for Download")}
+
+ ${_("The reports listed below are available for download, identified by UTC date and time of generation.")}
+
+
+ %if settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'):
+
+ ${_("The answer distribution report listed below is generated periodically by an automated background process. \
+ The report is cumulative, so answers submitted after the process starts are included in a subsequent report. \
+ The report is generated several times per day.")}
+
+ %endif
+
+ ## Translators: a table of URL links to report files appears after this sentence.
+
+ ${Text(_("{strong_start}Note{strong_end}: {ul_start}{li_start}To keep student data secure, you cannot save or \
+ email these links for direct access. Copies of links expire within 5 minutes.{li_end}{li_start}Report files \
+ are deleted 90 days after generation. If you will need access to old reports, download and store the files, \
+ in accordance with your institution's data security policies.{li_end}{ul_end}")).format(
+ strong_start=HTML(""),
+ strong_end=HTML(" "),
+ ul_start=HTML("
"),
+ li_start=HTML("
"),
+ li_end=HTML(" "),
+ )}
+
+
+
+
+
+
+%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+
+
+
${_("Pending Tasks")}
+
+
${_("The status for any active tasks appears in a table below.")}
+
+
+
+
+
+%endif
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html
new file mode 100644
index 0000000000..ea36196553
--- /dev/null
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html
@@ -0,0 +1,33 @@
+<%page args="section_data" expression_filter="h"/>
+<%namespace name='static' file='/static_content.html'/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+
+
+
+
+ View certificates
+
+ Download csv of
+ certificates
+
+
+
+
+
+
${_("Click to list certificates that are issued for this course:")}
+
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html
new file mode 100644
index 0000000000..5cd84df4a6
--- /dev/null
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html
@@ -0,0 +1,28 @@
+<%page args="section_data" expression_filter="h"/>
+<%namespace name='static' file='/static_content.html'/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
+
+
+ Learner status
+
+ Verified Learners Only
+ All Learners
+
+
+
+
+
${_("Click to generate a CSV grade report for all currently enrolled students.")}
+
+
+%endif
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html
new file mode 100644
index 0000000000..3299f6860c
--- /dev/null
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html
@@ -0,0 +1,44 @@
+<%page args="section_data" expression_filter="h"/>
+<%namespace name='static' file='/static_content.html'/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html
new file mode 100644
index 0000000000..71a3c2f45a
--- /dev/null
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html
@@ -0,0 +1,109 @@
+<%page args="section_data" expression_filter="h"/>
+<%namespace name='static' file='/static_content.html'/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+
+
+
+
+
+
+
+ Grading Configuration
+
+ Anonymized Student IDs
+
+ %if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'):
+ Profile Information
+
+
+ Learner
+ who can enroll
+
+
+ List enrolled students profile information
+
+ %if section_data['show_generate_proctored_exam_report_button']:
+ Proctored exam results
+
+ %endif
+ %if section_data['course_has_survey']:
+
+ Survey Result report
+
+ %endif
+ %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
+ ORA Data
+ report
+
+ Problem Grade report
+
+ %endif
+ %endif
+
+
+
+
+
+
+
+
${_("Click to display the grading configuration for the \
+ course. The grading configuration is the breakdown of graded subsections of the course \
+ (such as exams and problem sets), and can be changed on the 'Grading' \
+ page (under 'Settings') in Studio.")}
+
${_("Click to download a CSV of \
+ anonymized student IDs:")}
+
+
${_("For large courses, generating some reports can take \
+ several hours. When report generation is complete, a \
+ link that includes the date and time of generation appears in the table below. These reports are \
+ generated in the background, meaning it is OK to navigate away from this page while your report is \
+ generating.")}
+
+
${_("Please be patient and do not click these buttons \
+ multiple times. Clicking these buttons multiple times will significantly slow the generation \
+ process.")}
+
+ % if not disable_buttons:
+
${_("For smaller courses, click to list \
+ profile information for enrolled students directly on this page:")}
+ %endif
+
${_("Click to generate a CSV file of \
+ all students enrolled in this course, along with profile information such as email address and \
+ username:")}
+
+
${_("Click to generate a CSV file \
+ that lists learners who can enroll in the course but have not yet done so.")}
+
+
${_("Click to generate a CSV file \
+ of all proctored exam results in this course.")}
+
+
${_("Click to generate a CSV file of \
+ survey results for this course.")}
+
${_("Click to generate a CSV \
+ ORA grade report for all currently enrolled students.")}
+
${_("Click to generate a CSV \
+ problem grade report for all currently enrolled students.")}
+
+
+