From 52669c47f168ab06fb8aa81e352d8a2b7b155441 Mon Sep 17 00:00:00 2001 From: Ahtisham Shahid Date: Tue, 18 Aug 2020 13:15:30 +0500 Subject: [PATCH] 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 --- .../ProblemBrowser/components/Main/Main.jsx | 62 ++--- .../components/Main/Main.test.jsx | 73 ++--- .../Main/__snapshots__/Main.test.jsx.snap | 132 ++++----- .../tests/views/test_instructor_dashboard.py | 49 ++-- lms/djangoapps/instructor/toggles.py | 54 ++++ .../instructor/views/instructor_dashboard.py | 5 +- .../instructor_dashboard/data_download.html | 233 +++++++++++++++- .../instructor_dashboard/data_download_2.js | 257 ++++++++++++++++++ .../instructor_dashboard.js | 3 + .../data_download_spec.js | 212 +++++++++++---- lms/static/lms/js/spec/main.js | 1 + .../sass/course/instructor/_instructor_2.scss | 165 +++++++++-- .../data_download_2.html | 93 +++++++ .../data_download_2/certificates.html | 33 +++ .../data_download_2/grading.html | 28 ++ .../data_download_2/problem_report.html | 44 +++ .../data_download_2/reports.html | 109 ++++++++ 17 files changed, 1290 insertions(+), 263 deletions(-) create mode 100644 lms/djangoapps/instructor/toggles.py create mode 100644 lms/static/js/instructor_dashboard/data_download_2.js create mode 100644 lms/templates/instructor/instructor_dashboard_2/data_download_2.html create mode 100644 lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html create mode 100644 lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html create mode 100644 lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html create mode 100644 lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx index a9c9c5c61d..c919438665 100644 --- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx +++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.jsx @@ -1,15 +1,13 @@ /* global gettext */ -import { Button } from '@edx/paragon'; -import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer'; +import { Button, Icon } from '@edx/paragon'; +import { BlockBrowser } from 'BlockBrowser'; import * as PropTypes from 'prop-types'; import * as React from 'react'; -import { ReportStatusContainer } from '../ReportStatus/ReportStatusContainer'; export default class Main extends React.Component { constructor(props) { super(props); this.handleToggleDropdown = this.handleToggleDropdown.bind(this); - this.initiateReportGeneration = this.initiateReportGeneration.bind(this); this.state = { showDropdown: false, }; @@ -24,39 +22,31 @@ export default class Main extends React.Component { this.setState({ showDropdown: false }); } - initiateReportGeneration() { - this.props.createProblemResponsesReportTask( - this.props.problemResponsesEndpoint, - this.props.taskStatusEndpoint, - this.props.selectedBlock, - ); - } - render() { const { selectedBlock, onSelectBlock } = this.props; return ( -
-
-
- +
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} + + {selectedBlock || 'Select a section or problem'} + + + + + + + {this.state.showDropdown && + { + this.hideDropdown(); + onSelectBlock(blockId); + }} + />}
); } @@ -64,17 +54,13 @@ export default class Main extends React.Component { Main.propTypes = { courseId: PropTypes.string.isRequired, - createProblemResponsesReportTask: PropTypes.func.isRequired, excludeBlockTypes: PropTypes.arrayOf(PropTypes.string), fetchCourseBlocks: PropTypes.func.isRequired, - problemResponsesEndpoint: PropTypes.string.isRequired, onSelectBlock: PropTypes.func.isRequired, selectedBlock: PropTypes.string, - taskStatusEndpoint: PropTypes.string.isRequired, }; Main.defaultProps = { excludeBlockTypes: null, - selectedBlock: '', - timeout: null, + selectedBlock: null, }; diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx index 85fb8c72d6..c25973868b 100644 --- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx +++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/Main.test.jsx @@ -1,34 +1,25 @@ /* global jest,test,describe,expect */ import { Button } from '@edx/paragon'; -import BlockBrowserContainer from 'BlockBrowser/components/BlockBrowser/BlockBrowserContainer'; -import { Provider } from 'react-redux'; +import { BlockBrowser } from 'BlockBrowser'; import { shallow } from 'enzyme'; import React from 'react'; import renderer from 'react-test-renderer'; -import store from '../../data/store'; import Main from './Main'; describe('ProblemBrowser Main component', () => { const courseId = 'testcourse'; - const problemResponsesEndpoint = '/api/problem_responses/'; - const taskStatusEndpoint = '/api/task_status/'; const excludedBlockTypes = []; test('render with basic parameters', () => { const component = renderer.create( - -
- , +
, ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); @@ -36,18 +27,13 @@ describe('ProblemBrowser Main component', () => { test('render with selected block', () => { const component = renderer.create( - -
- , +
, ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); @@ -56,20 +42,15 @@ describe('ProblemBrowser Main component', () => { test('fetch course block on toggling dropdown', () => { const fetchCourseBlocksMock = jest.fn(); const component = renderer.create( - -
- , +
, ); - const instance = component.root.children[0].instance; + const instance = component.getInstance(); instance.handleToggleDropdown(); expect(fetchCourseBlocksMock.mock.calls.length).toBe(1); }); @@ -78,17 +59,13 @@ describe('ProblemBrowser Main component', () => { const component = shallow(
, ); - expect(component.find(BlockBrowserContainer).length).toBeFalsy(); - component.find(Button).find({ label: 'Select a section or problem' }).simulate('click'); - expect(component.find(BlockBrowserContainer).length).toBeTruthy(); + component.find('.problem-selector').simulate('click'); + expect(component.find(BlockBrowser)).toBeTruthy(); }); }); diff --git a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap index 115d583892..c6bbea41e6 100644 --- a/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap +++ b/lms/djangoapps/instructor/static/instructor/ProblemBrowser/components/Main/__snapshots__/Main.test.jsx.snap @@ -2,80 +2,90 @@ exports[`ProblemBrowser Main component render with basic parameters 1`] = `
-
- - - -
-
+ +
+ + +
`; exports[`ProblemBrowser Main component render with selected block 1`] = `
-
- - - -
-
+ +
+ + +
`; diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 3a140a057b..b221f8b734 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -26,6 +26,7 @@ from lms.djangoapps.courseware.tests.factories import StaffFactory, StudentModul from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info +from lms.djangoapps.instructor.toggles import DATA_DOWNLOAD_V2 from openedx.core.djangoapps.site_configuration.models import SiteConfiguration from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from student.models import CourseEnrollment @@ -136,31 +137,39 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT self.assertTrue(has_instructor_tab(org_researcher, self.course)) @ddt.data( - ('staff', False), - ('instructor', False), - ('data_researcher', True), - ('global_staff', True), + ('staff', False, False), + ('instructor', False, False), + ('data_researcher', True, False), + ('global_staff', True, False), + ('staff', False, True), + ('instructor', False, True), + ('data_researcher', True, True), + ('global_staff', True, True), ) @ddt.unpack - def test_data_download(self, access_role, can_access): + def test_data_download(self, access_role, can_access, waffle_status): """ Verify that the Data Download tab only shows up for certain roles """ - download_section = '' - 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 = '' + if waffle_status: + download_section = '' + user = UserFactory.create(is_staff=access_role == 'global_staff') + CourseAccessRoleFactory( + course_id=self.course.id, + user=user, + role=access_role, + org=self.course.id.org + ) + self.client.login(username=user.username, password="test") + response = self.client.get(self.url) + if can_access: + self.assertContains(response, download_section) + else: + self.assertNotContains(response, download_section) @override_settings(ANALYTICS_DASHBOARD_URL='http://example.com') @override_settings(ANALYTICS_DASHBOARD_NAME='Example') diff --git a/lms/djangoapps/instructor/toggles.py b/lms/djangoapps/instructor/toggles.py new file mode 100644 index 0000000000..3e4e58a423 --- /dev/null +++ b/lms/djangoapps/instructor/toggles.py @@ -0,0 +1,54 @@ +""" +Waffle flags for instructor dashboard. +""" + +from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleFlag + +WAFFLE_NAMESPACE = 'instructor' +# Namespace for instructor waffle flags. +WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=WAFFLE_NAMESPACE) + +# Waffle flag enable new data download UI on specific course. +# .. toggle_name: instructor.enable_data_download_v2 +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: instructor +# .. toggle_category: Instructor dashboard +# .. toggle_use_cases: incremental_release, open_edx +# .. toggle_creation_date: 2020-07-8 +# .. toggle_expiration_date: ?? +# .. toggle_warnings: ?? +# .. toggle_tickets: PROD-1309 +# .. toggle_status: supported +DATA_DOWNLOAD_V2 = CourseWaffleFlag( + waffle_namespace=WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='instructor_dashboard: '), + flag_name='enable_data_download_v2', +) + +# Waffle flag to use optimised is_small_course. +# .. toggle_name: verify_student.optimised_is_small_course +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Supports staged rollout to improved is_small_course method. +# .. toggle_category: instructor +# .. toggle_use_cases: incremental_release, open_edx +# .. toggle_creation_date: 2020-07-02 +# .. toggle_expiration_date: n/a +# .. toggle_warnings: n/a +# .. toggle_tickets: PROD-1740 +# .. toggle_status: supported +OPTIMISED_IS_SMALL_COURSE = WaffleFlag( + waffle_namespace=WAFFLE_FLAG_NAMESPACE, + flag_name='optimised_is_small_course', +) + + +def data_download_v2_is_enabled(course_key): + """ + check if data download v2 is enabled. + """ + return DATA_DOWNLOAD_V2.is_enabled(course_key) + + +def use_optimised_is_small_course(): + return OPTIMISED_IS_SMALL_COURSE.is_enabled() diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 7b39156a8a..ac587233f3 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -65,6 +65,7 @@ from xmodule.tabs import CourseTab from .tools import get_units_with_due_date, title_or_url from .. import permissions +from ..toggles import data_download_v2_is_enabled log = logging.getLogger(__name__) @@ -600,9 +601,9 @@ def _section_data_download(course, access): settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and course.enable_proctored_exams ) - + section_key = 'data_download_2' if data_download_v2_is_enabled(course_key) else 'data_download' section_data = { - 'section_key': 'data_download', + 'section_key': section_key, 'section_display_name': _('Data Download'), 'access': access, 'show_generate_proctored_exam_report_button': show_proctored_report_button, diff --git a/lms/static/js/fixtures/instructor_dashboard/data_download.html b/lms/static/js/fixtures/instructor_dashboard/data_download.html index ac50e51355..500ba6d31a 100644 --- a/lms/static/js/fixtures/instructor_dashboard/data_download.html +++ b/lms/static/js/fixtures/instructor_dashboard/data_download.html @@ -1,9 +1,224 @@ -
-

${_("Click to list certificates that are issued for this course:")}

- - - - -
-
-
\ No newline at end of file +
+
    + + + + + + +
+
+
+ ${_("NOTE")}: + Please select the report type and then click Download Report button +
+
+ +
+ + +
+ +
+ +
+

${_("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.")}

+ + + + + + + + + + + + + +
+ +
+
+
+ ${_("NOTE")}: + Please select the report type and then click Download Report button +
+ + + +
+

${_("Click to list certificates that are issued for this course:")}

+
+
+
+
+ ${_("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.")} +
+
+ ${static.renderReact( + component="ProblemBrowser", + id="react-block-listing", + props={ + "courseId": course.id, + "excludeBlockTypes": ['html', 'video', 'discussion'] + } + )} +
+ +

+ ${_("NOTE")}: + ${_("The generated report is limited to {max_entries} responses. If you expect more than {max_entries} " + "responses, try generating the report on a chapter-by-chapter, or problem-by-problem basis, or contact " + "your site administrator to increase the limit.").format(max_entries=max_entries)} +

+
+
+
+ ${_("NOTE")}: + Please select the report type and then click Download Report button +
+
+ +

Learner status

+ + + +
+

${_("Click to generate a CSV grade report for all currently enrolled students.")}

+
+
+
+
+ +
+ +
+
+
+
+
+ +

${_("Reports Available for Download")}

+

+ ${_("The reports listed below are available for download, identified by UTC date and time of generation.")} +

+ +

+ ${_("The answer distribution report listed below is generated periodically by an automated background process. \ + The report is cumulative, so answers submitted after the process starts are included in a subsequent report. \ + The report is generated several times per day.")} +

+

+ ${Text(_("{strong_start}Note{strong_end}: {ul_start}{li_start}To keep student data secure, you cannot save or \ + email these links for direct access. Copies of links expire within 5 minutes.{li_end}{li_start}Report files \ + are deleted 90 days after generation. If you will need access to old reports, download and store the files, \ + in accordance with your institution's data security policies.{li_end}{ul_end}")).format( + strong_start=HTML(""), + strong_end=HTML(""), + ul_start=HTML("

    "), + ul_end=HTML("
"), + li_start=HTML("
  • "), + li_end=HTML("
  • "), + )} +


    + +
    + +
    +
    +
    +

    ${_("Pending Tasks")}

    +
    +

    ${_("The status for any active tasks appears in a table below.")}

    +
    +
    +
    +
    +
    + diff --git a/lms/static/js/instructor_dashboard/data_download_2.js b/lms/static/js/instructor_dashboard/data_download_2.js new file mode 100644 index 0000000000..8416f090ba --- /dev/null +++ b/lms/static/js/instructor_dashboard/data_download_2.js @@ -0,0 +1,257 @@ +/* globals _, DataDownloadV2, PendingInstructorTasks, ReportDownloads */ + +(function() { + 'use strict'; + // eslint-disable-next-line no-unused-vars + var DataDownloadV2, PendingInstructorTasks, ReportDownloads, statusAjaxError; + + statusAjaxError = function() { + return window.InstructorDashboard.util.statusAjaxError.apply(this, arguments); + }; + + PendingInstructorTasks = function() { + return window.InstructorDashboard.util.PendingInstructorTasks; + }; + + ReportDownloads = function() { + return window.InstructorDashboard.util.ReportDownloads; + }; + + DataDownloadV2 = (function() { + function InstructorDashboardDataDownload($section) { + var dataDownloadObj = this; + this.$section = $section; + this.$section.data('wrapper', this); + this.$list_problem_responses_csv_input = this.$section.find("input[name='problem-location']"); + this.$download_display_text = $('.data-display-text'); + this.$download_request_response_error = $('.request-response-error'); + this.$download_display_table = $('.profile-data-display-table'); + this.$reports_request_response = $('.request-response'); + this.$reports_request_response_error = $('.request-response-error'); + this.report_downloads = new (ReportDownloads())(this.$section); + this.instructor_tasks = new (PendingInstructorTasks())(this.$section); + this.$download_report = $('.download-report'); + this.$gradeReportDownload = $('.grade-report-download'); + this.$report_type_selector = $('.report-type'); + this.$selection_informations = $('.selectionInfo'); + this.$data_display_table = $('.data-display-table-holder'); + this.$downloadProblemReport = $('#download-problem-report'); + this.$tabSwitch = $('.data-download-nav .btn-link'); + this.$selectedSection = $('#' + this.$tabSwitch.first().attr('data-section')); + this.$learnerStatus = $('.learner-status'); + + this.ERROR_MESSAGES = { + ORADataReport: gettext('Error generating ORA data report. Please try again.'), + problemGradeReport: gettext('Error generating problem grade report. Please try again.'), + profileInformation: gettext('Error generating student profile information. Please try again.'), + surveyResultReport: gettext('Error generating survey results. Please try again.'), + proctoredExamResults: gettext('Error generating proctored exam results. Please try again.'), + learnerWhoCanEnroll: gettext('Error generating list of students who may enroll. Please try again.'), + viewCertificates: gettext('Error getting issued certificates list.') + }; + + /** + * Removes text error/success messages and tables from UI + */ + this.clear_display = function() { + this.$download_display_text.empty(); + this.$download_display_table.empty(); + this.$download_request_response_error.empty(); + this.$reports_request_response.empty(); + this.$reports_request_response_error.empty(); + this.$data_display_table.empty(); + $('.msg-confirm').css({ + display: 'none' + }); + return $('.msg-error').css({ + display: 'none' + }); + }; + + this.clear_display(); + + /** + * Show and hide selected tab data + */ + this.$tabSwitch.click(function(event) { + var selectedSection = '#' + $(this).attr('data-section'); + event.preventDefault(); + $('.data-download-nav .btn-link').removeClass('active-section'); + $('section.tab-data').hide(); + $(selectedSection).show(); + $(this).addClass('active-section'); + + $(this).find('select').trigger('change'); + dataDownloadObj.$selectedSection = $(selectedSection); + + dataDownloadObj.clear_display(); + }); + + this.$tabSwitch.first().click(); + + /** + * on change of report select update show and hide related descriptions + */ + this.$report_type_selector.change(function() { + var selectedOption = dataDownloadObj.$report_type_selector.val(); + dataDownloadObj.$selection_informations.each(function(index, elem) { + if ($(elem).hasClass(selectedOption)) { + $(elem).show(); + } else { + $(elem).hide(); + } + }); + dataDownloadObj.clear_display(); + }); + + this.selectedOption = function() { + return dataDownloadObj.$selectedSection.find('select').find('option:selected'); + }; + + /** + * On click download button get selected option and pass it to handler function. + */ + this.downloadReportClickHandler = function() { + var selectedOption = dataDownloadObj.selectedOption(); + var errorMessage = dataDownloadObj.ERROR_MESSAGES[selectedOption.val()]; + + if (selectedOption.data('directdownload')) { + location.href = selectedOption.data('endpoint') + '?csv=true'; + } else if (selectedOption.data('datatable')) { + dataDownloadObj.renderDataTable(selectedOption); + } else { + dataDownloadObj.downloadCSV(selectedOption, errorMessage, false); + } + }; + this.$download_report.click(dataDownloadObj.downloadReportClickHandler); + + /** + * Call data endpoint and invoke buildDataTable to render Table UI. + * @param selected option element from report selector to get data-endpoint. + * @param errorMessage Error message in case endpoint call fail. + */ + this.renderDataTable = function(selected, errorMessage) { + var url = selected.data('endpoint'); + dataDownloadObj.clear_display(); + dataDownloadObj.$data_display_table.text(gettext('Loading data...')); + return $.ajax({ + type: 'POST', + url: url, + error: function(error) { + dataDownloadObj.OnError(error, errorMessage); + }, + success: function(data) { + dataDownloadObj.buildDataTable(data); + } + }); + }; + + + this.$downloadProblemReport.click(function() { + var data = {problem_location: dataDownloadObj.$list_problem_responses_csv_input.val()}; + dataDownloadObj.downloadCSV($(this), false, data); + }); + + this.$gradeReportDownload.click(function() { + var errorMessage = gettext('Error generating grades. Please try again.'); + var data = {verified_learners_only: dataDownloadObj.$learnerStatus.val()}; + dataDownloadObj.downloadCSV($(this), errorMessage, data); + }); + + /** + * Call data endpoint and render success/error message on dashboard UI. + */ + this.downloadCSV = function(selected, errorMessage, postData) { + var url = selected.data('endpoint'); + dataDownloadObj.clear_display(); + return $.ajax({ + type: 'POST', + dataType: 'json', + url: url, + data: postData, + error: function(error) { + dataDownloadObj.OnError(error, errorMessage); + }, + success: function(data) { + if (data.grading_config_summary) { + edx.HtmlUtils.setHtml( + dataDownloadObj.$download_display_text, edx.HtmlUtils.HTML(data.grading_config_summary)); + } else { + dataDownloadObj.$reports_request_response.text(data.status); + $('.msg-confirm').css({display: 'block'}); + } + } + }); + }; + + this.OnError = function(error, errorMessage) { + dataDownloadObj.clear_display(); + if (error.responseText) { + // eslint-disable-next-line no-param-reassign + errorMessage = JSON.parse(error.responseText); + } + dataDownloadObj.$download_request_response_error.text(errorMessage); + return dataDownloadObj.$download_request_response_error.css({ + display: 'block' + }); + }; + /** + * render data table on dashboard UI with given data. + */ + this.buildDataTable = function(data) { + var $tablePlaceholder, columns, feature, gridData, options; + dataDownloadObj.clear_display(); + options = { + enableCellNavigation: true, + enableColumnReorder: false, + forceFitColumns: true, + rowHeight: 35 + }; + columns = (function() { + var i, len, ref, results; + ref = data.queried_features; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + feature = ref[i]; + results.push({ + id: feature, + field: feature, + name: data.feature_names[feature] + }); + } + return results; + }()); + gridData = data.hasOwnProperty('students') ? data.students : data.certificates; + $tablePlaceholder = $('
    ', { + class: 'slickgrid' + }); + dataDownloadObj.$download_display_table.append($tablePlaceholder); + return new window.Slick.Grid($tablePlaceholder, gridData, columns, options); + }; + } + + InstructorDashboardDataDownload.prototype.onClickTitle = function() { + this.clear_display(); + this.instructor_tasks.task_poller.start(); + return this.report_downloads.downloads_poller.start(); + }; + + InstructorDashboardDataDownload.prototype.onExit = function() { + this.instructor_tasks.task_poller.stop(); + return this.report_downloads.downloads_poller.stop(); + }; + return InstructorDashboardDataDownload; + }()); + + _.defaults(window, { + InstructorDashboard: {} + }); + + _.defaults(window.InstructorDashboard, { + sections: {} + }); + + _.defaults(window.InstructorDashboard.sections, { + DataDownloadV2: DataDownloadV2 + }); +}).call(this); diff --git a/lms/static/js/instructor_dashboard/instructor_dashboard.js b/lms/static/js/instructor_dashboard/instructor_dashboard.js index c58e7610e9..b617bb8144 100644 --- a/lms/static/js/instructor_dashboard/instructor_dashboard.js +++ b/lms/static/js/instructor_dashboard/instructor_dashboard.js @@ -163,6 +163,9 @@ such that the value can be defined later than this assignment (file load order). }, { constructor: window.InstructorDashboard.sections.DataDownload, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download') + }, { + constructor: window.InstructorDashboard.sections.DataDownloadV2, + $element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download_2') }, { constructor: window.InstructorDashboard.sections.ECommerce, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#e-commerce') diff --git a/lms/static/js/spec/instructor_dashboard/data_download_spec.js b/lms/static/js/spec/instructor_dashboard/data_download_spec.js index 0e52e2c9b0..ad4c2bcbbf 100644 --- a/lms/static/js/spec/instructor_dashboard/data_download_spec.js +++ b/lms/static/js/spec/instructor_dashboard/data_download_spec.js @@ -1,70 +1,162 @@ -/* global define */ -define(['jquery', - 'js/instructor_dashboard/data_download', - 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'slick.grid'], - function($, DataDownload, AjaxHelpers) { - 'use strict'; - describe('edx.instructor_dashboard.data_download.DataDownload_Certificate', function() { - var url, data_download_certificate; +/* global define, DataDownload */ - beforeEach(function() { - loadFixtures('js/fixtures/instructor_dashboard/data_download.html'); - data_download_certificate = new window.DataDownload_Certificate($('.issued_certificates')); - url = '/courses/PU/FSc/2014_T4/instructor/api/get_issued_certificates'; - data_download_certificate.$list_issued_certificate_table_btn.data('endpoint', url); - }); +define([ + 'jquery', + 'js/instructor_dashboard/data_download_2', + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' +], + function($, id, AjaxHelper) { + 'use strict'; + describe('edx.instructor_dashboard.data_download', function() { + var requests, $selected, dataDownload, url, errorMessage; - it('show data on success callback', function() { - // Spy on AJAX requests - var requests = AjaxHelpers.requests(this); - var data = { - certificates: [{course_id: 'xyz_test', mode: 'honor'}], - queried_features: ['course_id', 'mode'], - feature_names: {course_id: 'Course ID', mode: ' Mode'} - }; + beforeEach(function() { + loadFixtures('js/fixtures/instructor_dashboard/data_download.html'); - data_download_certificate.$list_issued_certificate_table_btn.click(); - AjaxHelpers.expectJsonRequest(requests, 'POST', url); + dataDownload = window.InstructorDashboard.sections; + dataDownload.DataDownloadV2($('#data_download_2')); + window.InstructorDashboard.util.PendingInstructorTasks = function() { + return; + }; + requests = AjaxHelper.requests(this); + $selected = $(''); + url = $selected.data('endpoint'); + errorMessage = 'An Error is occurred with request'; + }); - // Simulate a success response from the server - AjaxHelpers.respondWithJson(requests, data); - expect(data_download_certificate.$certificate_display_table.html() - .indexOf('Course ID') !== -1).toBe(true); - expect(data_download_certificate.$certificate_display_table.html() - .indexOf('Mode') !== -1).toBe(true); - expect(data_download_certificate.$certificate_display_table.html() - .indexOf('xyz_test') !== -1).toBe(true); - expect(data_download_certificate.$certificate_display_table.html() - .indexOf('honor') !== -1).toBe(true); - }); + it('renders success message properly', function() { + dataDownload.downloadCSV($selected, errorMessage); - it('show error on failure callback', function() { - // Spy on AJAX requests - var requests = AjaxHelpers.requests(this); + AjaxHelper.expectRequest(requests, 'POST', url); + AjaxHelper.respondWithJson(requests, { + status: 'Request is succeeded' + }); + expect(dataDownload.$reports_request_response.text()).toContain('Request is succeeded'); + }); - data_download_certificate.$list_issued_certificate_table_btn.click(); - // Simulate a error response from the server - AjaxHelpers.respondWithError(requests); - expect(data_download_certificate.$certificates_request_response_error.text()) - .toEqual('Error getting issued certificates list.'); - }); - it('error should be clear from UI on success callback', function() { - var requests = AjaxHelpers.requests(this); - data_download_certificate.$list_issued_certificate_table_btn.click(); + it('renders grading config returned by the server in case of successful request ', function() { + dataDownload.downloadCSV($selected, errorMessage); - // Simulate a error response from the server - AjaxHelpers.respondWithError(requests); - expect(data_download_certificate.$certificates_request_response_error.text()) - .toEqual('Error getting issued certificates list.'); + AjaxHelper.expectRequest(requests, 'POST', url); + AjaxHelper.respondWithJson(requests, { + grading_config_summary: 'This is grading config' + }); + expect(dataDownload.$download_display_text.text()).toContain('This is grading config'); + }); - // Simulate a success response from the server - data_download_certificate.$list_issued_certificate_table_btn.click(); - AjaxHelpers.expectJsonRequest(requests, 'POST', url); + it('renders enrolled student list in case of successful request ', function() { + var data = { + available_features: [ + 'id', + 'username', + 'first_name', + 'last_name', + 'is_staff', + 'email', + 'date_joined', + 'last_login', + 'name', + 'language', + 'location', + 'year_of_birth', + 'gender', + 'level_of_education', + 'mailing_address', + 'goals', + 'meta', + 'city', + 'country' + ], + course_id: 'test_course_101', + feature_names: { + gender: 'Gender', + goals: 'Goals', + enrollment_mode: 'Enrollment Mode', + email: 'Email', + country: 'Country', + id: 'User ID', + mailing_address: 'Mailing Address', + last_login: 'Last Login', + date_joined: 'Date Joined', + location: 'Location', + city: 'City', + verification_status: 'Verification Status', + year_of_birth: 'Birth Year', + name: 'Name', + username: 'Username', + level_of_education: 'Level of Education', + language: 'Language' + }, + students: [ + { + gender: 'Male', + goals: 'Goal', + enrollment_mode: 'audit', + email: 'test@example.com', + country: 'PK', + year_of_birth: 'None', + id: '8', + mailing_address: 'None', + last_login: '2020-06-17T08:17:00.561Z', + date_joined: '2019-09-25T20:06:17.564Z', + location: 'None', + verification_status: 'N/A', + city: 'None', + name: 'None', + username: 'test', + level_of_education: 'None', + language: 'None' + } + ], + queried_features: [ + 'id', + 'username', + 'name', + 'email', + 'language', + 'location', + 'year_of_birth', + 'gender', + 'level_of_education', + 'mailing_address', + 'goals', + 'enrollment_mode', + 'verification_status', + 'last_login', + 'date_joined', + 'city', + 'country' + ], + students_count: 1 + }; + dataDownload.renderDataTable($selected, errorMessage); + AjaxHelper.expectRequest(requests, 'POST', url); + AjaxHelper.respondWithJson(requests, data); + // eslint-disable-next-line vars-on-top + var dataTable = dataDownload.$data_display_table.html(); + // eslint-disable-next-line vars-on-top + var existInHtml = function(value) { + expect(dataTable.indexOf(data.feature_names[value]) !== -1).toBe(false); + expect(dataTable.indexOf(data.students[0][value]) !== -1).toBe(false); + }; + data.queried_features.forEach(existInHtml); + }); - expect(data_download_certificate.$certificates_request_response_error.text()) - .not.toEqual('Error getting issued certificates list'); - }); - }); - }); + + it('calls renderDataTable function if data-datatable is true', function() { + $selected = $selected.attr('data-datatable', true); + spyOn(dataDownload, 'selectedOption').and.returnValue($selected); + spyOn(dataDownload, 'renderDataTable'); + dataDownload.downloadReportClickHandler(); + expect(dataDownload.renderDataTable).toHaveBeenCalled(); + }); + + it('calls downloadCSV function if no other data type is specified', function() { + spyOn(dataDownload, 'selectedOption').and.returnValue($selected); + spyOn(dataDownload, 'downloadCSV'); + dataDownload.downloadReportClickHandler(); + expect(dataDownload.downloadCSV).toHaveBeenCalled(); + }); + }); + }); diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 7c0cdb2c12..cb93e8bb0c 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -757,6 +757,7 @@ 'js/spec/financial-assistance/financial_assistance_form_view_spec.js', 'js/spec/groups/views/cohorts_spec.js', 'js/spec/groups/views/discussions_spec.js', + 'js/spec/instructor_dashboard/data_download_spec.js', 'js/spec/instructor_dashboard/certificates_bulk_exception_spec.js', 'js/spec/instructor_dashboard/certificates_exception_spec.js', 'js/spec/instructor_dashboard/certificates_invalidation_spec.js', diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 1a319abf9f..8c509acaae 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -313,28 +313,9 @@ } } } - - .report-generation-status { - .msg { - display: inherit; - - &.error { - color: $error-color; - } - - > div { - display: inline-block; - } - - a { - margin: 0 1rem; - - & > div { - display: inline-block; - } - } + .data-download-nav { + @extend .instructor-nav } - } } // elements - general @@ -1506,8 +1487,33 @@ // view - data download // -------------------- -.instructor-dashboard-wrapper-2 section.idash-section#data_download { - input { + + +.instructor-dashboard-wrapper-2 section.idash-section#data_download_2 { + .data-download-grid-container { + display:grid; + grid-template-columns:repeat(auto-fit, minmax(20rem, 1fr)); + grid-auto-rows: minmax(250px, auto); + div.card { + border-right: 2px solid grey; + border-bottom: 2px solid grey; + padding: 1em; + display: grid; + grid-template-rows: 1fr 3fr; + p.grid { + display: grid; + grid-template-columns: 1fr; + align-self: self-end; + } + .problem-browser { + display: grid; + grid-gap: 1em; + } + } + } + + + input { margin-bottom: 1em; line-height: 1.3em; } @@ -1531,8 +1537,79 @@ } } - #react-problem-report { - margin: $baseline 0; + .block-browser { + .header { + display: flex; + flex-direction: row; + align-items: center; + + .title { + margin: 0 0.5em; + } + } + + ul { + display: flex; + flex-direction: column; + padding: 0; + margin: 0; + + li { + display: flex; + flex-direction: row; + align-items: center; + margin: 0.25em 0; + + .block-name { + flex-grow: 1; + margin-right: 0.5em; + text-align: left; + } + } + } + } + + .problem-browser { + .block-browser { + position: absolute; + background: white; + padding: 5px; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + z-index: 2; + } + + input { + max-width: 800px; + width: 100%; + margin-bottom: 0; + } + } + +} + +.instructor-dashboard-wrapper-2 section.idash-section#data_download { + input { + margin-bottom: 1em; + line-height: 1.3em; + } + + .reports-download-container { + .data-display-table { + .slickgrid { + height: 400px; + } + } + + .report-downloads-table { + .slickgrid { + height: 300px; + padding: ($baseline/4); + } + // Disable horizontal scroll bar when grid only has 1 column. Remove this CSS class when more columns added. + .slick-viewport { + overflow-x: hidden !important; + } + } } .block-browser { @@ -2970,3 +3047,41 @@ div.staff_actions { color: theme-color("success"); } } + +.action-type-container p { + line-height: 2; +} + +.mb-15 { + margin-bottom: 20px; +} +.mb-20 { + margin-bottom: 20px; +} + +.download-report { + margin-left: 10px; +} + +.selector { + width: 315px; + height: 34px +} +.data-download-container .message { + border-radius: 1px; + padding: 10px 15px; + margin-bottom: 20px; + font-weight: 600; +} +.font-size-100 { + font-size: 100% +} + +.problem-selector{ + height: 34px; + min-width: 300px; + max-width: 780px; + border: 1px solid grey; + display: block; + padding: 8px; +} diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2.html new file mode 100644 index 0000000000..fe3e12cece --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2.html @@ -0,0 +1,93 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +
    +
      + + + %if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'): + + + %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']: + + %endif + %endif + +
    + <%include file="./data_download_2/reports.html" args="section_data=section_data, **context.kwargs" /> + + <%include file="./data_download_2/grading.html" args="section_data=section_data, **context.kwargs" /> + + %if settings.FEATURES.get('ENABLE_GRADE_DOWNLOADS'): + <%include file="./data_download_2/certificates.html" args="section_data=section_data, **context.kwargs" /> + <%include file="./data_download_2/problem_report.html" args="section_data=section_data, **context.kwargs" /> + %endif +
    +
    + +
    + +
    +
    +
    +
    +
    + +

    ${_("Reports Available for Download")}

    +

    + ${_("The reports listed below are available for download, identified by UTC date and time of generation.")} +

    + + %if settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'): +

    + ${_("The answer distribution report listed below is generated periodically by an automated background process. \ + The report is cumulative, so answers submitted after the process starts are included in a subsequent report. \ + The report is generated several times per day.")} +

    + %endif + + ## Translators: a table of URL links to report files appears after this sentence. +

    + ${Text(_("{strong_start}Note{strong_end}: {ul_start}{li_start}To keep student data secure, you cannot save or \ + email these links for direct access. Copies of links expire within 5 minutes.{li_end}{li_start}Report files \ + are deleted 90 days after generation. If you will need access to old reports, download and store the files, \ + in accordance with your institution's data security policies.{li_end}{ul_end}")).format( + strong_start=HTML(""), + strong_end=HTML(""), + ul_start=HTML("

      "), + ul_end=HTML("
    "), + li_start=HTML("
  • "), + li_end=HTML("
  • "), + )} +


    + +
    + +
    + +%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): +
    +
    +

    ${_("Pending Tasks")}

    +
    +

    ${_("The status for any active tasks appears in a table below.")}

    +
    +
    +
    +
    +
    +%endif + diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html new file mode 100644 index 0000000000..ea36196553 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/certificates.html @@ -0,0 +1,33 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +
    +
    + ${_("Note")}: + Please certificate report type option and then click Download Report button. +
    + + + + +
    +

    ${_("Click to list certificates that are issued for this course:")}

    +
    +
    diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html new file mode 100644 index 0000000000..5cd84df4a6 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/grading.html @@ -0,0 +1,28 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']: +
    +
    + ${_("Note")}: + Please select learner status and then click "Download Course Grade Report" button. +
    +

    Learner status

    + + + +
    +

    ${_("Click to generate a CSV grade report for all currently enrolled students.")}

    +
    +
    +%endif diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html new file mode 100644 index 0000000000..3299f6860c --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/problem_report.html @@ -0,0 +1,44 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +
    +
    + ${_("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.")} +
    +
    + ${static.renderReact( + component="ProblemBrowser", + id="react-block-listing", + props={ + "courseId": course.id, + "excludeBlockTypes": ['html', 'video', 'discussion'] + } + )} +
    + + + + + + + <% max_entries = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') %> + %if max_entries is not None: +

    + ${_("NOTE")}: + ${_("The generated report is limited to {max_entries} responses. If you expect more than {max_entries} " + "responses, try generating the report on a chapter-by-chapter, or problem-by-problem basis, or contact " + "your site administrator to increase the limit.").format(max_entries=max_entries)} +

    + %endif +
    diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html b/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html new file mode 100644 index 0000000000..71a3c2f45a --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/data_download_2/reports.html @@ -0,0 +1,109 @@ +<%page args="section_data" expression_filter="h"/> +<%namespace name='static' file='/static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +
    +
    + ${_("Note")}: + Please select the report type and then click "Download Report" button +
    +
    + +
    + + +
    + +
    + +
    +

    ${_("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.")}

    + + + + + + % if not disable_buttons: + + %endif + + + + + + + + + +
    + +