diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx index a9c9c5c61dd93daf9f08b2da5ab73470cf160e2c..c9194386653a860244270807725046495eb9b875 100644 --- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx +++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx @@ -1,15 +1,13 @@ /* global gettext */ -import { Button } from '@edx/paragon'; -import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer'; +import { Button, Icon } from '@edx/paragon'; +import { BlockBrowser } from 'BlockBrowser'; import * as PropTypes from 'prop-types'; import * as React from 'react'; -import { ReportStatusContainer } from '../ReportStatus/ReportStatusContainer'; export default class Main extends React.Component { constructor(props) { super(props); this.handleToggleDropdown = this.handleToggleDropdown.bind(this); - this.initiateReportGeneration = this.initiateReportGeneration.bind(this); this.state = { showDropdown: false, }; @@ -24,39 +22,31 @@ export default class Main extends React.Component { this.setState({ showDropdown: false }); } - initiateReportGeneration() { - this.props.createProblemResponsesReportTask( - this.props.problemResponsesEndpoint, - this.props.taskStatusEndpoint, - this.props.selectedBlock, - ); - } - render() { const { selectedBlock, onSelectBlock } = this.props; return ( - <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, }; diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx index 85fb8c72d605c937971297c98f68b726fcd26a84..c25973868b154f3f00ddb5b95544bcf926c81fcb 100644 --- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx +++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx @@ -1,34 +1,25 @@ /* global jest,test,describe,expect */ import { Button } from '@edx/paragon'; -import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer'; -import { Provider } from 'react-redux'; +import { BlockBrowser } from 'BlockBrowser'; import { shallow } from 'enzyme'; import React from 'react'; import renderer from 'react-test-renderer'; -import store from '../../data/store'; import Main from './Main'; describe('ProblemBrowser Main component', () => { const courseId = 'testcourse'; - const problemResponsesEndpoint = '/api/problem_responses/'; - const taskStatusEndpoint = '/api/task_status/'; const excludedBlockTypes = []; test('render with basic parameters', () => { const component = renderer.create( - <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(); }); }); diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap index 115d5838925978f777beca16bbfa8cb94fb00496..c6bbea41e60aba72d1ee1da7e9863a09c31ddcd4 100644 --- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap +++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap @@ -2,80 +2,90 @@ exports[`ProblemBrowser Main component render with basic parameters 1`] = ` <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" - > - 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" + <span> + some-selected-block + </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="some-selected-block" /> </div> `; diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 3a140a057b2b69001127df2ef14c5701081708c1..b221f8b7340e8a6e779cbb227e09045ea7f0b557 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -26,6 +26,7 @@ from lms.djangoapps.courseware.tests.factories import StaffFactory, StudentModul from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info +from lms.djangoapps.instructor.toggles import DATA_DOWNLOAD_V2 from openedx.core.djangoapps.site_configuration.models import SiteConfiguration from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from student.models import CourseEnrollment @@ -136,31 +137,39 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT self.assertTrue(has_instructor_tab(org_researcher, self.course)) @ddt.data( - ('staff', False), - ('instructor', False), - ('data_researcher', True), - ('global_staff', True), + ('staff', False, False), + ('instructor', False, False), + ('data_researcher', True, False), + ('global_staff', True, False), + ('staff', False, True), + ('instructor', False, True), + ('data_researcher', True, True), + ('global_staff', True, True), ) @ddt.unpack - def test_data_download(self, access_role, can_access): + def test_data_download(self, access_role, can_access, waffle_status): """ Verify that the Data Download tab only shows up for certain roles """ - download_section = '<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') diff --git a/lms/djangoapps/instructor/toggles.py b/lms/djangoapps/instructor/toggles.py new file mode 100644 index 0000000000000000000000000000000000000000..3e4e58a42399f71a20eb0bf4b7a7a088cc637f38 --- /dev/null +++ b/lms/djangoapps/instructor/toggles.py @@ -0,0 +1,54 @@ +""" +Waffle flags for instructor dashboard. +""" + +from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleFlag + +WAFFLE_NAMESPACE = 'instructor' +# Namespace for instructor waffle flags. +WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=WAFFLE_NAMESPACE) + +# Waffle flag enable new data download UI on specific course. +# .. toggle_name: instructor.enable_data_download_v2 +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: instructor +# .. toggle_category: Instructor dashboard +# .. toggle_use_cases: incremental_release, open_edx +# .. toggle_creation_date: 2020-07-8 +# .. toggle_expiration_date: ?? +# .. toggle_warnings: ?? +# .. toggle_tickets: PROD-1309 +# .. toggle_status: supported +DATA_DOWNLOAD_V2 = CourseWaffleFlag( + waffle_namespace=WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='instructor_dashboard: '), + flag_name='enable_data_download_v2', +) + +# Waffle flag to use optimised is_small_course. +# .. toggle_name: verify_student.optimised_is_small_course +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Supports staged rollout to improved is_small_course method. +# .. toggle_category: instructor +# .. toggle_use_cases: incremental_release, open_edx +# .. toggle_creation_date: 2020-07-02 +# .. toggle_expiration_date: n/a +# .. toggle_warnings: n/a +# .. toggle_tickets: PROD-1740 +# .. toggle_status: supported +OPTIMISED_IS_SMALL_COURSE = WaffleFlag( + waffle_namespace=WAFFLE_FLAG_NAMESPACE, + flag_name='optimised_is_small_course', +) + + +def data_download_v2_is_enabled(course_key): + """ + check if data download v2 is enabled. + """ + return DATA_DOWNLOAD_V2.is_enabled(course_key) + + +def use_optimised_is_small_course(): + return OPTIMISED_IS_SMALL_COURSE.is_enabled() diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 7b39156a8a244e82b86c6a825b467e6ca989a49d..ac587233f39873b73f27683cfca4f8f37594fe0f 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -65,6 +65,7 @@ from xmodule.tabs import CourseTab from .tools import get_units_with_due_date, title_or_url from .. import permissions +from ..toggles import data_download_v2_is_enabled log = logging.getLogger(__name__) @@ -600,9 +601,9 @@ def _section_data_download(course, access): settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and course.enable_proctored_exams ) - + section_key = 'data_download_2' if data_download_v2_is_enabled(course_key) else 'data_download' section_data = { - 'section_key': 'data_download', + 'section_key': section_key, 'section_display_name': _('Data Download'), 'access': access, 'show_generate_proctored_exam_report_button': show_proctored_report_button, diff --git a/lms/static/js/fixtures/instructor_dashboard/data_download.html b/lms/static/js/fixtures/instructor_dashboard/data_download.html index ac50e51355b6703660b28e05936f60af6b83d847..500ba6d31a8ac54aceabcbc9e9b612cbc6ba9328 100644 --- a/lms/static/js/fixtures/instructor_dashboard/data_download.html +++ b/lms/static/js/fixtures/instructor_dashboard/data_download.html @@ -1,9 +1,224 @@ -<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> \ No newline at end of file +<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> + diff --git a/lms/static/js/instructor_dashboard/data_download_2.js b/lms/static/js/instructor_dashboard/data_download_2.js new file mode 100644 index 0000000000000000000000000000000000000000..8416f090ba612d98fd3d13d4aa4a59ef452b9d4f --- /dev/null +++ b/lms/static/js/instructor_dashboard/data_download_2.js @@ -0,0 +1,257 @@ +/* globals _, DataDownloadV2, PendingInstructorTasks, ReportDownloads */ + +(function() { + 'use strict'; + // eslint-disable-next-line no-unused-vars + var DataDownloadV2, PendingInstructorTasks, ReportDownloads, statusAjaxError; + + statusAjaxError = function() { + return window.InstructorDashboard.util.statusAjaxError.apply(this, arguments); + }; + + PendingInstructorTasks = function() { + return window.InstructorDashboard.util.PendingInstructorTasks; + }; + + ReportDownloads = function() { + return window.InstructorDashboard.util.ReportDownloads; + }; + + DataDownloadV2 = (function() { + function InstructorDashboardDataDownload($section) { + var dataDownloadObj = this; + this.$section = $section; + this.$section.data('wrapper', this); + this.$list_problem_responses_csv_input = this.$section.find("input[name='problem-location']"); + this.$download_display_text = $('.data-display-text'); + this.$download_request_response_error = $('.request-response-error'); + this.$download_display_table = $('.profile-data-display-table'); + this.$reports_request_response = $('.request-response'); + this.$reports_request_response_error = $('.request-response-error'); + this.report_downloads = new (ReportDownloads())(this.$section); + this.instructor_tasks = new (PendingInstructorTasks())(this.$section); + this.$download_report = $('.download-report'); + this.$gradeReportDownload = $('.grade-report-download'); + this.$report_type_selector = $('.report-type'); + this.$selection_informations = $('.selectionInfo'); + this.$data_display_table = $('.data-display-table-holder'); + this.$downloadProblemReport = $('#download-problem-report'); + this.$tabSwitch = $('.data-download-nav .btn-link'); + this.$selectedSection = $('#' + this.$tabSwitch.first().attr('data-section')); + this.$learnerStatus = $('.learner-status'); + + this.ERROR_MESSAGES = { + ORADataReport: gettext('Error generating ORA data report. Please try again.'), + problemGradeReport: gettext('Error generating problem grade report. Please try again.'), + profileInformation: gettext('Error generating student profile information. Please try again.'), + surveyResultReport: gettext('Error generating survey results. Please try again.'), + proctoredExamResults: gettext('Error generating proctored exam results. Please try again.'), + learnerWhoCanEnroll: gettext('Error generating list of students who may enroll. Please try again.'), + viewCertificates: gettext('Error getting issued certificates list.') + }; + + /** + * Removes text error/success messages and tables from UI + */ + this.clear_display = function() { + this.$download_display_text.empty(); + this.$download_display_table.empty(); + this.$download_request_response_error.empty(); + this.$reports_request_response.empty(); + this.$reports_request_response_error.empty(); + this.$data_display_table.empty(); + $('.msg-confirm').css({ + display: 'none' + }); + return $('.msg-error').css({ + display: 'none' + }); + }; + + this.clear_display(); + + /** + * Show and hide selected tab data + */ + this.$tabSwitch.click(function(event) { + var selectedSection = '#' + $(this).attr('data-section'); + event.preventDefault(); + $('.data-download-nav .btn-link').removeClass('active-section'); + $('section.tab-data').hide(); + $(selectedSection).show(); + $(this).addClass('active-section'); + + $(this).find('select').trigger('change'); + dataDownloadObj.$selectedSection = $(selectedSection); + + dataDownloadObj.clear_display(); + }); + + this.$tabSwitch.first().click(); + + /** + * on change of report select update show and hide related descriptions + */ + this.$report_type_selector.change(function() { + var selectedOption = dataDownloadObj.$report_type_selector.val(); + dataDownloadObj.$selection_informations.each(function(index, elem) { + if ($(elem).hasClass(selectedOption)) { + $(elem).show(); + } else { + $(elem).hide(); + } + }); + dataDownloadObj.clear_display(); + }); + + this.selectedOption = function() { + return dataDownloadObj.$selectedSection.find('select').find('option:selected'); + }; + + /** + * On click download button get selected option and pass it to handler function. + */ + this.downloadReportClickHandler = function() { + var selectedOption = dataDownloadObj.selectedOption(); + var errorMessage = dataDownloadObj.ERROR_MESSAGES[selectedOption.val()]; + + if (selectedOption.data('directdownload')) { + location.href = selectedOption.data('endpoint') + '?csv=true'; + } else if (selectedOption.data('datatable')) { + dataDownloadObj.renderDataTable(selectedOption); + } else { + dataDownloadObj.downloadCSV(selectedOption, errorMessage, false); + } + }; + this.$download_report.click(dataDownloadObj.downloadReportClickHandler); + + /** + * Call data endpoint and invoke buildDataTable to render Table UI. + * @param selected option element from report selector to get data-endpoint. + * @param errorMessage Error message in case endpoint call fail. + */ + this.renderDataTable = function(selected, errorMessage) { + var url = selected.data('endpoint'); + dataDownloadObj.clear_display(); + dataDownloadObj.$data_display_table.text(gettext('Loading data...')); + return $.ajax({ + type: 'POST', + url: url, + error: function(error) { + dataDownloadObj.OnError(error, errorMessage); + }, + success: function(data) { + dataDownloadObj.buildDataTable(data); + } + }); + }; + + + this.$downloadProblemReport.click(function() { + var data = {problem_location: dataDownloadObj.$list_problem_responses_csv_input.val()}; + dataDownloadObj.downloadCSV($(this), false, data); + }); + + this.$gradeReportDownload.click(function() { + var errorMessage = gettext('Error generating grades. Please try again.'); + var data = {verified_learners_only: dataDownloadObj.$learnerStatus.val()}; + dataDownloadObj.downloadCSV($(this), errorMessage, data); + }); + + /** + * Call data endpoint and render success/error message on dashboard UI. + */ + this.downloadCSV = function(selected, errorMessage, postData) { + var url = selected.data('endpoint'); + dataDownloadObj.clear_display(); + return $.ajax({ + type: 'POST', + dataType: 'json', + url: url, + data: postData, + error: function(error) { + dataDownloadObj.OnError(error, errorMessage); + }, + success: function(data) { + if (data.grading_config_summary) { + edx.HtmlUtils.setHtml( + dataDownloadObj.$download_display_text, edx.HtmlUtils.HTML(data.grading_config_summary)); + } else { + dataDownloadObj.$reports_request_response.text(data.status); + $('.msg-confirm').css({display: 'block'}); + } + } + }); + }; + + this.OnError = function(error, errorMessage) { + dataDownloadObj.clear_display(); + if (error.responseText) { + // eslint-disable-next-line no-param-reassign + errorMessage = JSON.parse(error.responseText); + } + dataDownloadObj.$download_request_response_error.text(errorMessage); + return dataDownloadObj.$download_request_response_error.css({ + display: 'block' + }); + }; + /** + * render data table on dashboard UI with given data. + */ + this.buildDataTable = function(data) { + var $tablePlaceholder, columns, feature, gridData, options; + dataDownloadObj.clear_display(); + options = { + enableCellNavigation: true, + enableColumnReorder: false, + forceFitColumns: true, + rowHeight: 35 + }; + columns = (function() { + var i, len, ref, results; + ref = data.queried_features; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + feature = ref[i]; + results.push({ + id: feature, + field: feature, + name: data.feature_names[feature] + }); + } + return results; + }()); + gridData = data.hasOwnProperty('students') ? data.students : data.certificates; + $tablePlaceholder = $('<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); diff --git a/lms/static/js/instructor_dashboard/instructor_dashboard.js b/lms/static/js/instructor_dashboard/instructor_dashboard.js index c58e7610e98b11a8e9f8ee05694b8263e468982c..b617bb8144bd5f26c146ca251a276e18e8915b53 100644 --- a/lms/static/js/instructor_dashboard/instructor_dashboard.js +++ b/lms/static/js/instructor_dashboard/instructor_dashboard.js @@ -163,6 +163,9 @@ such that the value can be defined later than this assignment (file load order). }, { constructor: window.InstructorDashboard.sections.DataDownload, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download') + }, { + constructor: window.InstructorDashboard.sections.DataDownloadV2, + $element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download_2') }, { constructor: window.InstructorDashboard.sections.ECommerce, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#e-commerce') diff --git a/lms/static/js/spec/instructor_dashboard/data_download_spec.js b/lms/static/js/spec/instructor_dashboard/data_download_spec.js index 0e52e2c9b0e1f41fe9996b23cb44e135d56047bb..ad4c2bcbbf2b548a4cb9e8a6fee6b8380b49347e 100644 --- a/lms/static/js/spec/instructor_dashboard/data_download_spec.js +++ b/lms/static/js/spec/instructor_dashboard/data_download_spec.js @@ -1,70 +1,162 @@ -/* global define */ -define(['jquery', - 'js/instructor_dashboard/data_download', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'slick.grid'], - function($, DataDownload, AjaxHelpers) { - 'use strict'; - describe('edx.instructor_dashboard.data_download.DataDownload_Certificate', function() { - var url, data_download_certificate; +/* global define, DataDownload */ - beforeEach(function() { - loadFixtures('js/fixtures/instructor_dashboard/data_download.html'); - data_download_certificate = new window.DataDownload_Certificate($('.issued_certificates')); - url = '/courses/PU/FSc/2014_T4/instructor/api/get_issued_certificates'; - data_download_certificate.$list_issued_certificate_table_btn.data('endpoint', url); - }); +define([ + 'jquery', + 'js/instructor_dashboard/data_download_2', + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' +], + function($, id, AjaxHelper) { + 'use strict'; + describe('edx.instructor_dashboard.data_download', function() { + var requests, $selected, dataDownload, url, errorMessage; - it('show data on success callback', function() { - // Spy on AJAX requests - var requests = AjaxHelpers.requests(this); - var data = { - certificates: [{course_id: 'xyz_test', mode: 'honor'}], - queried_features: ['course_id', 'mode'], - feature_names: {course_id: 'Course ID', mode: ' Mode'} - }; + beforeEach(function() { + loadFixtures('js/fixtures/instructor_dashboard/data_download.html'); - data_download_certificate.$list_issued_certificate_table_btn.click(); - AjaxHelpers.expectJsonRequest(requests, 'POST', url); + dataDownload = window.InstructorDashboard.sections; + dataDownload.DataDownloadV2($('#data_download_2')); + window.InstructorDashboard.util.PendingInstructorTasks = function() { + return; + }; + requests = AjaxHelper.requests(this); + $selected = $('<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(); + }); + }); + }); diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 7c0cdb2c12666e10854634ef85d87123324bd877..cb93e8bb0c5716829c87482acd3127d70724b857 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -757,6 +757,7 @@ 'js/spec/financial-assistance/financial_assistance_form_view_spec.js', 'js/spec/groups/views/cohorts_spec.js', 'js/spec/groups/views/discussions_spec.js', + 'js/spec/instructor_dashboard/data_download_spec.js', 'js/spec/instructor_dashboard/certificates_bulk_exception_spec.js', 'js/spec/instructor_dashboard/certificates_exception_spec.js', 'js/spec/instructor_dashboard/certificates_invalidation_spec.js', diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 1a319abf9f65d9bebb22552ea470a642e48c6fa7..8c509acaaee3b81230b000fccf4c0c0fabed9374 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -313,28 +313,9 @@ } } } - - .report-generation-status { - .msg { - display: inherit; - - &.error { - color: $error-color; - } - - > div { - display: inline-block; - } - - a { - margin: 0 1rem; - - & > div { - display: inline-block; - } - } + .data-download-nav { + @extend .instructor-nav } - } } // elements - general @@ -1506,8 +1487,33 @@ // view - data download // -------------------- -.instructor-dashboard-wrapper-2 section.idash-section#data_download { - input { + + +.instructor-dashboard-wrapper-2 section.idash-section#data_download_2 { + .data-download-grid-container { + display:grid; + grid-template-columns:repeat(auto-fit, minmax(20rem, 1fr)); + grid-auto-rows: minmax(250px, auto); + div.card { + border-right: 2px solid grey; + border-bottom: 2px solid grey; + padding: 1em; + display: grid; + grid-template-rows: 1fr 3fr; + p.grid { + display: grid; + grid-template-columns: 1fr; + align-self: self-end; + } + .problem-browser { + display: grid; + grid-gap: 1em; + } + } + } + + + input { margin-bottom: 1em; line-height: 1.3em; } @@ -1531,8 +1537,79 @@ } } - #react-problem-report { - margin: $baseline 0; + .block-browser { + .header { + display: flex; + flex-direction: row; + align-items: center; + + .title { + margin: 0 0.5em; + } + } + + ul { + display: flex; + flex-direction: column; + padding: 0; + margin: 0; + + li { + display: flex; + flex-direction: row; + align-items: center; + margin: 0.25em 0; + + .block-name { + flex-grow: 1; + margin-right: 0.5em; + text-align: left; + } + } + } + } + + .problem-browser { + .block-browser { + position: absolute; + background: white; + padding: 5px; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + z-index: 2; + } + + input { + max-width: 800px; + width: 100%; + margin-bottom: 0; + } + } + +} + +.instructor-dashboard-wrapper-2 section.idash-section#data_download { + input { + margin-bottom: 1em; + line-height: 1.3em; + } + + .reports-download-container { + .data-display-table { + .slickgrid { + height: 400px; + } + } + + .report-downloads-table { + .slickgrid { + height: 300px; + padding: ($baseline/4); + } + // Disable horizontal scroll bar when grid only has 1 column. Remove this CSS class when more columns added. + .slick-viewport { + overflow-x: hidden !important; + } + } } .block-browser { @@ -2970,3 +3047,41 @@ div.staff_actions { color: theme-color("success"); } } + +.action-type-container p { + line-height: 2; +} + +.mb-15 { + margin-bottom: 20px; +} +.mb-20 { + margin-bottom: 20px; +} + +.download-report { + margin-left: 10px; +} + +.selector { + width: 315px; + height: 34px +} +.data-download-container .message { + border-radius: 1px; + padding: 10px 15px; + margin-bottom: 20px; + font-weight: 600; +} +.font-size-100 { + font-size: 100% +} + +.problem-selector{ + height: 34px; + min-width: 300px; + max-width: 780px; + border: 1px solid grey; + display: block; + padding: 8px; +} diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2.html new file mode 100644 index 0000000000000000000000000000000000000000..fe3e12cece006e223a0c14c9c2dfdfce34840fd0 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2.html @@ -0,0 +1,93 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +<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 + diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html new file mode 100644 index 0000000000000000000000000000000000000000..ea36196553984ff9c41333881149e1c1de24baea --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html @@ -0,0 +1,33 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +<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> diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html new file mode 100644 index 0000000000000000000000000000000000000000..5cd84df4a67046a52473406f6eeac4fb2cd84425 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html @@ -0,0 +1,28 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']: +<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 diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html new file mode 100644 index 0000000000000000000000000000000000000000..3299f6860cc632ad2f75f13e2de03bc7204c8d1f --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html @@ -0,0 +1,44 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +<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> diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html new file mode 100644 index 0000000000000000000000000000000000000000..71a3c2f45a4e8c09258302f3233f3bd7fbb5361a --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html @@ -0,0 +1,109 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +<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>