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

updated css

temp fixex

Updated js code for data download

updated js hooks for new UI

fixed ui and navigation

reset paver file

Removed unused changes

Initial tests added

Initial tests added

fixed style issues

Created new tests for data download

Fixed A11y and quality issues

Updated test file and removed new

fixed Accesibility issues

fixed code style in spec

removed old data download file

Moved problem grade report

Updated html to fix accessiblity issue

Fixed accessiblity issues

Created waffle flag for data download

added doc strign in doc

renamed waffles file

Break down Html and fixed tests

Removed extra js and updated comments

Removed extra js and updated comments

renamed var fixed styling

fixed js test fail

Fixed styling issues

updated description texts

Updated problem selector UI

Fixed Jest test for react component

removed depricated default param

added class instead of style

updated snapshot

Co-authored-by: Awais Jibran <awaisdar001@gmail.com>
This commit is contained in:
Ahtisham Shahid
2020-08-18 13:15:30 +05:00
committed by GitHub
parent f0ae71ab76
commit 52669c47f1
17 changed files with 1290 additions and 263 deletions

View File

@@ -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 (
<div className="problem-browser-container">
<div className="problem-browser">
<Button
onClick={this.handleToggleDropdown}
label={gettext('Select a section or problem')}
/>
<input type="text" name="problem-location" value={selectedBlock} disabled />
{this.state.showDropdown &&
<BlockBrowserContainer
onSelectBlock={(blockId) => {
this.hideDropdown();
onSelectBlock(blockId);
}}
/>}
<Button
onClick={this.initiateReportGeneration}
name="list-problem-responses-csv"
label={gettext('Create a report of problem responses')}
/>
</div>
<ReportStatusContainer />
<div className="problem-browser">
{/* 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>
<input type="text" name="problem-location" value={selectedBlock} disabled style={{ display: 'none' }} />
{this.state.showDropdown &&
<BlockBrowser onSelectBlock={(blockId) => {
this.hideDropdown();
onSelectBlock(blockId);
}}
/>}
</div>
);
}
@@ -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,
};

View File

@@ -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(
<Provider store={store}>
<Main
courseId={courseId}
createProblemResponsesReportTask={jest.fn()}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={jest.fn()}
problemResponsesEndpoint={problemResponsesEndpoint}
onSelectBlock={jest.fn()}
selectedBlock={null}
taskStatusEndpoint={taskStatusEndpoint}
/>
</Provider>,
<Main
courseId={courseId}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={jest.fn()}
onSelectBlock={jest.fn()}
selectedBlock={null}
/>,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
@@ -36,18 +27,13 @@ describe('ProblemBrowser Main component', () => {
test('render with selected block', () => {
const component = renderer.create(
<Provider store={store}>
<Main
courseId={courseId}
createProblemResponsesReportTask={jest.fn()}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={jest.fn()}
problemResponsesEndpoint={problemResponsesEndpoint}
onSelectBlock={jest.fn()}
selectedBlock={'some-selected-block'}
taskStatusEndpoint={taskStatusEndpoint}
/>
</Provider>,
<Main
courseId={courseId}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={jest.fn()}
onSelectBlock={jest.fn()}
selectedBlock={'some-selected-block'}
/>,
);
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(
<Provider store={store}>
<Main
courseId={courseId}
createProblemResponsesReportTask={jest.fn()}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={fetchCourseBlocksMock}
problemResponsesEndpoint={problemResponsesEndpoint}
onSelectBlock={jest.fn()}
selectedBlock={'some-selected-block'}
taskStatusEndpoint={taskStatusEndpoint}
/>
</Provider>,
<Main
courseId={courseId}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={fetchCourseBlocksMock}
onSelectBlock={jest.fn()}
selectedBlock={'some-selected-block'}
/>,
);
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(
<Main
courseId={courseId}
createProblemResponsesReportTask={jest.fn()}
excludeBlockTypes={excludedBlockTypes}
fetchCourseBlocks={jest.fn()}
problemResponsesEndpoint={problemResponsesEndpoint}
onSelectBlock={jest.fn()}
selectedBlock={'some-selected-block'}
taskStatusEndpoint={taskStatusEndpoint}
/>,
);
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();
});
});

View File

@@ -2,80 +2,90 @@
exports[`ProblemBrowser Main component render with basic parameters 1`] = `
<div
className="problem-browser-container"
className="problem-browser"
>
<div
className="problem-browser"
<span
className={
Array [
"problem-selector",
]
}
onClick={[Function]}
>
<button
className="btn"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<span>
Select a section or problem
</button>
<input
disabled={true}
name="problem-location"
type="text"
value={null}
/>
<button
className="btn"
name="list-problem-responses-csv"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
</span>
<span
className={
Array [
"pull-right",
]
}
>
Create a report of problem responses
</button>
</div>
<div
aria-live="polite"
className="report-generation-status"
<div>
<span
aria-hidden={true}
className="fa fa-sort"
id="Icon2"
/>
</div>
</span>
</span>
<input
disabled={true}
name="problem-location"
style={
Object {
"display": "none",
}
}
type="text"
value={null}
/>
</div>
`;
exports[`ProblemBrowser Main component render with selected block 1`] = `
<div
className="problem-browser-container"
className="problem-browser"
>
<div
className="problem-browser"
<span
className={
Array [
"problem-selector",
]
}
onClick={[Function]}
>
<button
className="btn"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
<span>
some-selected-block
</span>
<span
className={
Array [
"pull-right",
]
}
>
Select a section or problem
</button>
<input
disabled={true}
name="problem-location"
type="text"
value="some-selected-block"
/>
<button
className="btn"
name="list-problem-responses-csv"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Create a report of problem responses
</button>
</div>
<div
aria-live="polite"
className="report-generation-status"
<div>
<span
aria-hidden={true}
className="fa fa-sort"
id="Icon2"
/>
</div>
</span>
</span>
<input
disabled={true}
name="problem-location"
style={
Object {
"display": "none",
}
}
type="text"
value="some-selected-block"
/>
</div>
`;

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, 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 = $('<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(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();
});
});
});

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,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;
}

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,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
%>
<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-block-listing",
props={
"courseId": course.id,
"excludeBlockTypes": ['html', 'video', 'discussion']
}
)}
</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 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>