New vs Old Data-Download UI. (#24840)

Created new UI for Data download in instructors dashboard

Co-authored-by: Awais Jibran <awaisdar001@gmail.com>
This commit is contained in:
Ahtisham Shahid
2020-09-21 17:25:47 +05:00
committed by GitHub
parent 8e813ab7e8
commit cf735e411d
20 changed files with 1231 additions and 119 deletions

View File

@@ -1,5 +1,5 @@
/* global gettext */
import { Button } from '@edx/paragon';
import { Button, Icon } from '@edx/paragon';
import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer';
import * as PropTypes from 'prop-types';
import * as React from 'react';
@@ -29,35 +29,56 @@ export default class Main extends React.Component {
this.props.problemResponsesEndpoint,
this.props.taskStatusEndpoint,
this.props.reportDownloadEndpoint,
this.props.selectedBlock,
);
this.props.selectedBlock);
}
render() {
const { selectedBlock, onSelectBlock } = this.props;
let selectorType = <Button onClick={this.handleToggleDropdown} label={gettext('Select a section or problem')} />;
if (this.props.showBtnUi === 'false') {
selectorType =
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
(<span
onClick={this.handleToggleDropdown}
className={['problem-selector']}
>
<span>{selectedBlock || 'Select a section or problem'}</span>
<span className={['pull-right']}>
<Icon
className={['fa', 'fa-sort']}
/>
</span>
</span>);
}
return (
<div className="problem-browser-container">
<div className="problem-browser">
<Button
onClick={this.handleToggleDropdown}
label={gettext('Select a section or problem')}
{selectorType}
<input
type="text"
name="problem-location"
value={selectedBlock}
disabled
hidden={this.props.showBtnUi === 'false'}
/>
<input type="text" name="problem-location" value={selectedBlock} disabled />
{this.state.showDropdown &&
<BlockBrowserContainer
onSelectBlock={(blockId) => {
this.hideDropdown();
onSelectBlock(blockId);
}}
/>}
<BlockBrowserContainer
onSelectBlock={(blockId) => {
this.hideDropdown();
onSelectBlock(blockId);
}}
/>}
<Button
onClick={this.initiateReportGeneration}
name="list-problem-responses-csv"
label={gettext('Create a report of problem responses')}
/>
<ReportStatusContainer />
</div>
<ReportStatusContainer />
</div>
);
}
@@ -73,6 +94,7 @@ Main.propTypes = {
selectedBlock: PropTypes.string,
taskStatusEndpoint: PropTypes.string.isRequired,
reportDownloadEndpoint: PropTypes.string.isRequired,
ShowBtnUi: PropTypes.string.isRequired,
};
Main.defaultProps = {

View File

@@ -18,6 +18,7 @@ exports[`ProblemBrowser Main component render with basic parameters 1`] = `
</button>
<input
disabled={true}
hidden={false}
name="problem-location"
type="text"
value={null}
@@ -32,11 +33,11 @@ exports[`ProblemBrowser Main component render with basic parameters 1`] = `
>
Create a report of problem responses
</button>
<div
aria-live="polite"
className="report-generation-status"
/>
</div>
<div
aria-live="polite"
className="report-generation-status"
/>
</div>
`;
@@ -58,6 +59,7 @@ exports[`ProblemBrowser Main component render with selected block 1`] = `
</button>
<input
disabled={true}
hidden={false}
name="problem-location"
type="text"
value="some-selected-block"
@@ -72,10 +74,10 @@ exports[`ProblemBrowser Main component render with selected block 1`] = `
>
Create a report of problem responses
</button>
<div
aria-live="polite"
className="report-generation-status"
/>
</div>
<div
aria-live="polite"
className="report-generation-status"
/>
</div>
`;

View File

@@ -14,7 +14,7 @@ const ReportStatus = ({ error, succeeded, inProgress, reportPath }) => {
const successMessage = (
<div className="msg success">
{gettext('Your report has being successfully generated.')}
{gettext('Your report has been successfully generated.')}
{reportPath &&
<a href={reportPath}>
<Icon hidden className={['fa', 'fa-link']} />

View File

@@ -42,7 +42,7 @@ exports[`ReportStatus component render success status 1`] = `
<div
className="msg success"
>
Your report has being successfully generated.
Your report has been successfully generated.
<a
href="/some/report/path.csv"
>

View File

@@ -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 = '<li class="nav-item"><button type="button" class="btn-link data_download" '\
'data-section="data_download">Data Download</button></li>'
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 = '<li class="nav-item"><button type="button" class="btn-link data_download" ' \
'data-section="data_download">Data Download</button></li>'
if waffle_status:
download_section = '<li class="nav-item"><button type="button" class="btn-link data_download_2" '\
'data-section="data_download_2">Data Download</button></li>'
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')

View File

@@ -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()

View File

@@ -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,

View File

@@ -1,9 +1,224 @@
<div class="issued_certificates">
<p>${_("Click to list certificates that are issued for this course:")}</p>
<span>
<input type="button" name="issued-certificates-list" value="View Certificates Issued" >
<input type="button" name="issued-certificates-csv" value="Download CSV of Certificates Issued" >
</span>
<div class="data-display-table certificate-data-display-table" id="data-issued-certificates-table"></div>
<div class="issued-certificates-error request-response-error msg msg-error copy"></div>
</div>
<div class="data-download-container action-type-container">
<ul class="data-download-nav">
<li class="nav-item ">
<button type="button" class="btn-link reports active-section" data-section="reports">Reports</button>
</li>
<li class="nav-item">
<button type="button" class="btn-link problem-report" data-section="problem">Problem Report</button>
</li>
<li class="nav-item">
<button type="button" class="btn-link certificates" data-section="certificate">Certificates</button>
</li>
<li class="nav-item">
<button type="button" class="btn-link grading" data-section="grading">Grading</button>
</li>
</ul>
<section id="reports" class="idash-section tab-data" aria-labelledby="header-reports">
<h6 class="mb-15" id="header-reports">
<strong>${_("NOTE")}: </strong>
Please select the report type and then click Download Report button
</h6>
<div class="mb-15">
<div class="mb-5">
<select class="report-type selector">
<option value="gradingConfiguration"
data-endpoint="">
Grading Configuration
</option>
<option value="listAnonymizeStudentIDs" data-endpoint=""
class=""
aria-disabled="">Anonymized Student IDs
</option>
<option value="profileInformation"
data-endpoint=""
data-csv="true">Profile Information
</option>
<option value="learnerWhoCanEnroll"
data-endpoint="" data-csv="true">
Learner
who can enroll
</option>
<option value="listEnrolledPeople"
data-endpoint="">
List enrolled students profile information
</option>
<option value="proctoredExamResults"
data-endpoint="">Proctored exam results
</option>
<option value="surveyResultReport"
data-endpoint="">
Survey Result report
</option>
<option value="ORADataReport" data-graderelated="true"
data-endpoint="">ORA Data
report
</option>
<option data-graderelated="true" value="problemGradeReport"
data-endpoint="">Problem Grade report
</option>
</select>
<button class="btn-brand download-report" type="button" value="download report">Download
report
</button>
</div>
</div>
<div>
<p class="selectionInfo gradingConfiguration">${_("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.")}</p>
<p hidden="hidden" class="selectionInfo listAnonymizeStudentIDs">${_("Click to download a CSV of \
anonymized student IDs:")}</p>
<p hidden="hidden" class="selectionInfo reports"> ${_("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.")}</p>
<p hidden="hidden" class="selectionInfo reports">${_("Please be patient and do not click these buttons \
multiple times. Clicking these buttons multiple times will significantly slow the generation \
process.")}
</p>
<p hidden="hidden" class="selectionInfo listEnrolledPeople">${_("For smaller courses, click to list \
profile information for enrolled students directly on this page:")}</p>
<p hidden="hidden" class="selectionInfo reports profileInformation">${_("Click to generate a CSV file of \
all students enrolled in this course, along with profile information such as email address and \
username:")}</p>
<p hidden="hidden" class="selectionInfo reports learnerWhoCanEnroll">${_("Click to generate a CSV file \
that lists learners who can enroll in the course but have not yet done so.")}</p>
<p hidden="hidden" class="selectionInfo reports proctoredExamResults">${_("Click to generate a CSV file \
of all proctored exam results in this course.")}</p>
<p hidden="hidden" class="selectionInfo reports surveyResultReport">${_("Click to generate a CSV file of \
survey results for this course.")}</p>
</div>
</section>
<section id="certificate" class="idash-section tab-data" aria-labelledby="header-cert">
<h6 class="mb-15" id="header-cert">
<strong>${_("NOTE")}: </strong>
Please select the report type and then click Download Report button
</h6>
<select class="report-type selector">
<option value="viewCertificates" data-csv="false"
data-endpoint="">View certificates
</option>
<option value="downloadCertificates" data-csv="true"
data-endpoint="">Download csv of
certificates
</option>
</select>
<button class="mb-20 btn-brand download-report" type="button" value="download report">Download
report
</button>
<div>
<p>${_("Click to list certificates that are issued for this course:")}</p>
</div>
</section>
<section id="problem" class="idash-section tab-data" aria-labelledby="header-problem">
<h6 class="mb-20" id="header-problem">
${_("Select a problem to generate a CSV \
file that lists all student answers to the problem. You also select a section or chapter to include \
results of all problems in that section or chapter.")}
</h6>
<div class="mb-15 problems">
${static.renderReact(
component="ProblemBrowser",
id="react-block-listing",
props={
"courseId": course.id,
"excludeBlockTypes": ['html', 'video', 'discussion']
}
)}
</div>
<button data-endpoint="" id="download-problem-report"
class="btn-brand mb-20" type="button" value="download report">Download
report
</button>
<p class="mb-15">
<strong>${_("NOTE")}: </strong>
${_("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)}
</p>
</section>
<section id="grading" class="idash-section tab-data" aria-labelledby="header-grading">
<h6 class="mb-15" id="header-grading">
<strong>${_("NOTE")}: </strong>
Please select the report type and then click Download Report button
</h6>
<br>
<p>Learner status</p>
<select class="learner-status selector">
<option value="true">Verified Learners Only</option>
<option value="false">All Learners</option>
</select>
<button class="mb-20 btn-brand grade-report-download" type="button"
value="download report"
data-endpoint="">Download Report
</button>
<div>
<p>${_("Click to generate a CSV grade report for all currently enrolled students.")}</p>
</div>
</section>
<div class="request-response message msg-confirm copy" id="report-request-response"></div>
<div class="request-response-error message msg-error copy" id="report-request-response-error"></div>
</div>
<div class="reports-download-container action-type-container">
<div class="data-display-text" id="data-grade-config-text"></div>
<div class="data-display-table profile-data-display-table" id="data-student-profiles-table"></div>
<div class="data-display-table data-display-table-holder" id="data-issued-certificates-table"></div>
<hr>
<h3 class="hd hd-3">${_("Reports Available for Download")}</h3>
<p>
${_("The reports listed below are available for download, identified by UTC date and time of generation.")}
</p>
<p>
${_("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.")}
</p>
<p>
${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>"),
strong_end=HTML("</strong>"),
ul_start=HTML("<ul>"),
ul_end=HTML("</ul>"),
li_start=HTML("<li>"),
li_end=HTML("</li>"),
)}
</p><br>
<div class="report-downloads-table" id="report-downloads-table"
data-endpoint=""></div>
</div>
<div id="data_download_2" class="running-tasks-container action-type-container">
<hr>
<h3 class="hd hd-3">${_("Pending Tasks")}</h3>
<div class="running-tasks-section">
<p>${_("The status for any active tasks appears in a table below.")} </p>
<br/>
<div class="running-tasks-table" data-endpoint=""></div>
</div>
<div class="no-pending-tasks-message"></div>
</div>

View File

@@ -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 = $('<div/>', {
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);

View File

@@ -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')

View File

@@ -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, dataDownloadCertificates;
/* global define, DataDownload */
beforeEach(function() {
loadFixtures('js/fixtures/instructor_dashboard/data_download.html');
dataDownloadCertificates = new window.DataDownload_Certificate($('.issued_certificates'));
url = '/courses/PU/FSc/2014_T4/instructor/api/get_issued_certificates';
dataDownloadCertificates.$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');
dataDownloadCertificates.$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 = $('<option data-endpoint="api/url/fake"></option>');
url = $selected.data('endpoint');
errorMessage = 'An Error is occurred with request';
});
// Simulate a success response from the server
AjaxHelpers.respondWithJson(requests, data);
expect(dataDownloadCertificates.$certificate_display_table.html()
.indexOf('Course ID') !== -1).toBe(true);
expect(dataDownloadCertificates.$certificate_display_table.html()
.indexOf('Mode') !== -1).toBe(true);
expect(dataDownloadCertificates.$certificate_display_table.html()
.indexOf('xyz_test') !== -1).toBe(true);
expect(dataDownloadCertificates.$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');
});
dataDownloadCertificates.$list_issued_certificate_table_btn.click();
// Simulate a error response from the server
AjaxHelpers.respondWithError(requests);
expect(dataDownloadCertificates.$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);
dataDownloadCertificates.$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(dataDownloadCertificates.$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
dataDownloadCertificates.$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(dataDownloadCertificates.$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();
});
});
});

View File

@@ -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',

View File

@@ -313,7 +313,9 @@
}
}
}
.data-download-nav {
@extend .instructor-nav
}
.report-generation-status {
.msg {
display: inherit;
@@ -1506,6 +1508,106 @@
// view - data download
// --------------------
.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;
}
.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 {
.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;
@@ -2970,3 +3072,46 @@ 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;
width: 780px;
border: 1px solid grey;
display: block;
padding: 8px;
margin-right: 10px;
float: left;
}
.block-browser{
margin-top: 34px;
}

View File

@@ -7,4 +7,3 @@ from openedx.core.djangolib.markup import HTML, Text
%>
<p>${Text(_("We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible."))}</p>

View File

@@ -58,7 +58,7 @@ from openedx.core.djangolib.markup import HTML, Text
</p>
%endif
<div class="problems">
<div class="mb-15 problems">
${static.renderReact(
component="ProblemBrowser",
id="react-problem-report",
@@ -67,7 +67,8 @@ from openedx.core.djangolib.markup import HTML, Text
"excludeBlockTypes": ['html', 'video', 'discussion'],
"problemResponsesEndpoint": section_data['get_problem_responses_url'],
"taskStatusEndpoint": "/instructor_task_status",
"reportDownloadEndpoint": section_data['list_report_downloads_url']
"reportDownloadEndpoint": section_data['list_report_downloads_url'],
"ShowBtnUi": "true",
}
)}
</div>

View File

@@ -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
%>
<div class="data-download-container action-type-container">
<ul class="data-download-nav">
<li class="nav-item ">
<button type="button" class="btn-link reports active-section" data-section="reports">Reports</button>
</li>
%if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'):
<li class="nav-item">
<button type="button" class="btn-link problem-report" data-section="problem">Problem Report</button>
</li>
<li class="nav-item">
<button type="button" class="btn-link certificates" data-section="certificate">Certificates</button>
</li>
%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
<li class="nav-item">
<button type="button" class="btn-link grading" data-section="grading">Grading</button>
</li>
%endif
%endif
</ul>
<%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
<div class="request-response message msg-confirm copy" id="report-request-response"></div>
<div class="request-response-error message msg-error copy" id="report-request-response-error"></div>
</div>
<div class="reports-download-container action-type-container">
<div class="data-display-text" id="data-grade-config-text"></div>
<div class="data-display-table profile-data-display-table" id="data-student-profiles-table"></div>
<div class="data-display-table data-display-table-holder" id="data-issued-certificates-table"></div>
<hr>
<h3 class="hd hd-3">${_("Reports Available for Download")}</h3>
<p>
${_("The reports listed below are available for download, identified by UTC date and time of generation.")}
</p>
%if settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'):
<p>
${_("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.")}
</p>
%endif
## Translators: a table of URL links to report files appears after this sentence.
<p>
${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>"),
strong_end=HTML("</strong>"),
ul_start=HTML("<ul>"),
ul_end=HTML("</ul>"),
li_start=HTML("<li>"),
li_end=HTML("</li>"),
)}
</p><br>
<div class="report-downloads-table" id="report-downloads-table"
data-endpoint="${ section_data['list_report_downloads_url'] }"></div>
</div>
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
<div class="running-tasks-container action-type-container">
<hr>
<h3 class="hd hd-3">${_("Pending Tasks")}</h3>
<div class="running-tasks-section">
<p>${_("The status for any active tasks appears in a table below.")} </p>
<br/>
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div>
<div class="no-pending-tasks-message"></div>
</div>
%endif

View File

@@ -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
%>
<section id="certificate" class="idash-section tab-data" aria-labelledby="header-cert">
<h6 class="mb-15 font-size-100" id="header-cert">
<strong>${_("Note")}: </strong>
Please certificate report type option and then click Download Report button.
</h6>
<select class="report-type selector">
<option value="viewCertificates" data-csv="false"
data-datatable="true"
data-endpoint="${ section_data['get_issued_certificates_url'] }">View certificates
</option>
<option value="downloadCertificates"
data-csv="true"
data-directdownload="true"
data-endpoint="${ section_data['get_issued_certificates_url'] }">Download csv of
certificates
</option>
</select>
<input type="button"
value="Download Report"
class="mb-20 download-report">
<div>
<p>${_("Click to list certificates that are issued for this course:")}</p>
</div>
</section>

View File

@@ -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']:
<section id="grading" class="idash-section tab-data" aria-labelledby="header-grading">
<h6 class="mb-15 font-size-100" id="header-grading">
<strong>${_("Note")}: </strong>
Please select learner status and then click "Download Course Grade Report" button.
</h6>
<p>Learner status</p>
<select class="learner-status selector">
<option value="true">Verified Learners Only</option>
<option value="false">All Learners</option>
</select>
<input data-endpoint="${ section_data['calculate_grades_csv_url'] }"
type="button"
value="Download Course Grade Report"
class="mb-20 grade-report-download">
<div>
<p>${_("Click to generate a CSV grade report for all currently enrolled students.")}</p>
</div>
</section>
%endif

View File

@@ -0,0 +1,48 @@
<%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
%>
<section id="problem" class="idash-section tab-data" aria-labelledby="header-problem">
<h6 class="mb-20 font-size-100" id="header-problem">
${_("Select a problem to generate a CSV \
file that lists all student answers to the problem. You also select a section or chapter to include \
results of all problems in that section or chapter.")}
</h6>
<div class="mb-15 problems">
${static.renderReact(
component="ProblemBrowser",
id="react-problem-report",
props={
"courseId": course.id,
"excludeBlockTypes": ['html', 'video', 'discussion'],
"problemResponsesEndpoint": section_data['get_problem_responses_url'],
"taskStatusEndpoint": "/instructor_task_status",
"reportDownloadEndpoint": section_data['list_report_downloads_url'],
"showBtnUi": "false"
}
)}
</div>
<!-- <button data-endpoint="${ section_data['get_problem_responses_url'] }" id="download-problem-report"-->
<!-- class="btn-brand mb-20" type="button" value="download report">Download-->
<!-- report-->
<!-- </button>-->
<input hidden data-endpoint="${ section_data['get_problem_responses_url'] }"
type="button"
value="Download Report"
id="download-problem-report"
class="download-report mb-20"
style="margin-left: 0">
<% max_entries = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') %>
%if max_entries is not None:
<p class="mb-15">
<strong>${_("NOTE")}: </strong>
${_("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)}
</p>
%endif
</section>

View File

@@ -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
%>
<section id="reports" class="idash-section tab-data" aria-labelledby="header-reports">
<h6 class="mb-15 font-size-100" id="header-reports">
<strong>${_("Note")}: </strong>
Please select the report type and then click "Download Report" button
</h6>
<div class="mb-15">
<div class="">
<select class="report-type selector">
<option value="gradingConfiguration"
data-endpoint="${ section_data['get_grading_config_url'] }">
Grading Configuration
</option>
<option value="listAnonymizeStudentIDs"
data-endpoint="${ section_data['get_anon_ids_url'] }"
data-directdownload="true"
class="${'is-disabled' if disable_buttons else ''}"
aria-disabled="${'true' if disable_buttons else 'false'}">Anonymized Student IDs
</option>
%if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'):
<option value="profileInformation"
data-endpoint="${ section_data['get_students_features_url'] + '/csv' }"
data-csv="true">Profile Information
</option>
<option value="learnerWhoCanEnroll"
data-endpoint="${ section_data['get_students_who_may_enroll_url'] }" data-csv="true">
Learner
who can enroll
</option>
<option value="listEnrolledPeople"
data-endpoint="${ section_data['get_students_features_url'] }"
data-datatable="true">
List enrolled students profile information
</option>
%if section_data['show_generate_proctored_exam_report_button']:
<option value="proctoredExamResults"
data-endpoint="${ section_data['list_proctored_results_url'] }">Proctored exam results
</option>
%endif
%if section_data['course_has_survey']:
<option value="surveyResultReport"
data-endpoint="${ section_data['course_survey_results_url'] }">
Survey Result report
</option>
%endif
%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
<option value="ORADataReport" data-graderelated="true"
data-endpoint="${ section_data['export_ora2_data_url'] }">ORA Data
report
</option>
<option data-graderelated="true" value="problemGradeReport"
data-endpoint="${ section_data['problem_grade_report_url'] }">Problem Grade report
</option>
%endif
%endif
</select>
<input type="button" value="Download Report" class="download-report ml-10">
</div>
</div>
<div>
<p class="selectionInfo gradingConfiguration">${_("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.")}</p>
<p hidden="hidden" class="selectionInfo listAnonymizeStudentIDs">${_("Click to download a CSV of \
anonymized student IDs:")}</p>
<p hidden="hidden" class="selectionInfo reports"> ${_("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.")}</p>
<p hidden="hidden" class="selectionInfo reports">${_("Please be patient and do not click these buttons \
multiple times. Clicking these buttons multiple times will significantly slow the generation \
process.")}
</p>
% if not disable_buttons:
<p hidden="hidden" class="selectionInfo listEnrolledPeople">${_("For smaller courses, click to list \
profile information for enrolled students directly on this page:")}</p>
%endif
<p hidden="hidden" class="selectionInfo reports profileInformation">${_("Click to generate a CSV file of \
all students enrolled in this course, along with profile information such as email address and \
username:")}</p>
<p hidden="hidden" class="selectionInfo reports learnerWhoCanEnroll">${_("Click to generate a CSV file \
that lists learners who can enroll in the course but have not yet done so.")}</p>
<p hidden="hidden" class="selectionInfo reports proctoredExamResults">${_("Click to generate a CSV file \
of all proctored exam results in this course.")}</p>
<p hidden="hidden" class="selectionInfo reports surveyResultReport">${_("Click to generate a CSV file of \
survey results for this course.")}</p>
<p hidden="hidden" class="selectionInfo reports ORADataReport">${_("Click to generate a CSV \
ORA grade report for all currently enrolled students.")}</p>
<p hidden="hidden" class="selectionInfo reports problemGradeReport">${_("Click to generate a CSV \
problem grade report for all currently enrolled students.")}</p>
</div>
</section>