From e2423386cbdf984967d290e0657c7059ba7ae4a2 Mon Sep 17 00:00:00 2001 From: Sarina Canelake <sarina@edx.org> Date: Wed, 20 Nov 2013 11:05:10 -0500 Subject: [PATCH] UX for Data Download tab on instructor dash Restrict grade report generation to 'is_superuser' users (can be overridden with feature flag ALLOW_COURSE_STAFF_GRADE_DOWNLOADS); all staff users can download generated files. LMS-58 --- lms/djangoapps/instructor/views/api.py | 6 +- lms/envs/common.py | 3 + .../instructor_dashboard/data_download.coffee | 91 ++++++++++++------- .../src/instructor_dashboard/util.coffee | 2 +- .../sass/course/instructor/_instructor_2.scss | 24 ++++- .../instructor_dashboard_2/data_download.html | 50 +++++++--- .../instructor_dashboard_2/student_admin.html | 12 +-- 7 files changed, 127 insertions(+), 61 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index cac8ebf9811..d4180eef0e0 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -778,10 +778,12 @@ def calculate_grades_csv(request, course_id): """ try: instructor_task.api.submit_calculate_grades_csv(request, course_id) - return JsonResponse({"status" : "Grade calculation started"}) + success_status = _("Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.") + return JsonResponse({"status": success_status}) except AlreadyRunningError: + already_running_status = _("A grade report generation task is already in progress. Check the 'Pending Instructor Tasks' table for the status of the task. When completed, the report will be available for download in the table below.") return JsonResponse({ - "status" : "Grade calculation already running" + "status": already_running_status }) diff --git a/lms/envs/common.py b/lms/envs/common.py index 73de4c00fcf..a8575b89b6d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -196,6 +196,9 @@ MITX_FEATURES = { # Grade calculation started from the new instructor dashboard will write # grades CSV files to S3 and give links for downloads. 'ENABLE_S3_GRADE_DOWNLOADS' : True, + # Give course staff unrestricted access to grade downloads (if set to False, + # only edX superusers can perform the downloads) + 'ALLOW_COURSE_STAFF_GRADE_DOWNLOADS' : False, } # Used for A/B testing diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index e39b2f60c78..22bda4151dd 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -17,17 +17,23 @@ class DataDownload # this object to call event handlers like 'onClickTitle' @$section.data 'wrapper', @ # gather elements - @$display = @$section.find '.data-display' - @$display_text = @$display.find '.data-display-text' - @$display_table = @$display.find '.data-display-table' - @$request_response_error = @$display.find '.request-response-error' - @$list_studs_btn = @$section.find("input[name='list-profiles']'") @$list_anon_btn = @$section.find("input[name='list-anon-ids']'") @$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") + @$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'") + + # response areas + @$download = @$section.find '.data-download-container' + @$download_display_text = @$download.find '.data-display-text' + @$download_display_table = @$download.find '.data-display-table' + @$download_request_response_error = @$download.find '.request-response-error' + @$grades = @$section.find '.grades-download-container' + @$grades_request_response = @$grades.find '.request-response' + @$grades_request_response_error = @$grades.find '.request-response-error' @grade_downloads = new GradeDownloads(@$section) @instructor_tasks = new (PendingInstructorTasks()) @$section + @clear_display() # attach click handlers # The list-anon case is always CSV @@ -46,8 +52,9 @@ class DataDownload url += '/csv' location.href = url else + # Dynamically generate slickgrid table for displaying student profile information @clear_display() - @$display_table.text 'Loading...' + @$download_display_table.text gettext('Loading...') # fetch user list $.ajax @@ -55,7 +62,7 @@ class DataDownload url: url error: std_ajax_err => @clear_display() - @$request_response_error.text "Error getting student list." + @$download_request_response_error.text gettext("Error getting student list.") success: (data) => @clear_display() @@ -64,12 +71,13 @@ class DataDownload enableCellNavigation: true enableColumnReorder: false forceFitColumns: true + rowHeight: 35 columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features) grid_data = data.students $table_placeholder = $ '<div/>', class: 'slickgrid' - @$display_table.append $table_placeholder + @$download_display_table.append $table_placeholder grid = new Slick.Grid($table_placeholder, grid_data, columns, options) # grid.autosizeColumns() @@ -81,13 +89,31 @@ class DataDownload url: url error: std_ajax_err => @clear_display() - @$request_response_error.text "Error getting grading configuration." + @$download_request_response_error.text gettext("Error retrieving grading configuration.") success: (data) => @clear_display() - @$display_text.html data['grading_config_summary'] + @$download_display_text.html data['grading_config_summary'] + + @$calculate_grades_csv_btn.click (e) => + # Clear any CSS styling from the request-response areas + #$(".msg-confirm").css({"display":"none"}) + #$(".msg-error").css({"display":"none"}) + @clear_display() + url = @$calculate_grades_csv_btn.data 'endpoint' + $.ajax + dataType: 'json' + url: url + error: std_ajax_err => + @$grades_request_response_error.text gettext("Error generating grades. Please try again.") + $(".msg-error").css({"display":"block"}) + success: (data) => + @$grades_request_response.text data['status'] + $(".msg-confirm").css({"display":"block"}) # handler for when the section title is clicked. onClickTitle: -> + # Clear display of anything that was here before + @clear_display() @instructor_tasks.task_poller.start() @grade_downloads.downloads_poller.start() @@ -97,36 +123,32 @@ class DataDownload @grade_downloads.downloads_poller.stop() clear_display: -> - @$display_text.empty() - @$display_table.empty() - @$request_response_error.empty() + # Clear any generated tables, warning messages, etc. + @$download_display_text.empty() + @$download_display_table.empty() + @$download_request_response_error.empty() + @$grades_request_response.empty() + @$grades_request_response_error.empty() + # Clear any CSS styling from the request-response areas + $(".msg-confirm").css({"display":"none"}) + $(".msg-error").css({"display":"none"}) class GradeDownloads ### Grade Downloads -- links expire quickly, so we refresh every 5 mins #### constructor: (@$section) -> - @$grade_downloads_table = @$section.find ".grade-downloads-table" - @$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'") - @$display = @$section.find '.data-display' - @$display_text = @$display.find '.data-display-text' - @$request_response_error = @$display.find '.request-response-error' + + @$grades = @$section.find '.grades-download-container' + @$grades_request_response = @$grades.find '.request-response' + @$grades_request_response_error = @$grades.find '.request-response-error' + @$grade_downloads_table = @$grades.find ".grade-downloads-table" POLL_INTERVAL = 1000 * 60 * 5 # 5 minutes in ms @downloads_poller = new window.InstructorDashboard.util.IntervalManager( POLL_INTERVAL, => @reload_grade_downloads() ) - @$calculate_grades_csv_btn.click (e) => - url = @$calculate_grades_csv_btn.data 'endpoint' - $.ajax - dataType: 'json' - url: url - error: std_ajax_err => - @$request_response_error.text "Error generating grades." - success: (data) => - @$display_text.html data['status'] - reload_grade_downloads: -> endpoint = @$grade_downloads_table.data 'endpoint' $.ajax @@ -145,15 +167,17 @@ class GradeDownloads options = enableCellNavigation: true enableColumnReorder: false - autoHeight: true + rowHeight: 30 forceFitColumns: true columns = [ id: 'link' field: 'link' - name: 'File' - sortable: false, - minWidth: 200, + name: gettext('File Name (Newest First)') + toolTip: gettext("Links are generated on demand and expire within 5 minutes due to the sensitive nature of student grade information.") + sortable: false + minWidth: 150 + cssClass: "file-download-link" formatter: (row, cell, value, columnDef, dataContext) -> '<a href="' + dataContext['url'] + '">' + dataContext['name'] + '</a>' ] @@ -161,8 +185,7 @@ class GradeDownloads $table_placeholder = $ '<div/>', class: 'slickgrid' @$grade_downloads_table.append $table_placeholder grid = new Slick.Grid($table_placeholder, grade_downloads_data, columns, options) - - + grid.autosizeColumns() # export for use diff --git a/lms/static/coffee/src/instructor_dashboard/util.coffee b/lms/static/coffee/src/instructor_dashboard/util.coffee index af5b99f4198..14a2f2b8ca4 100644 --- a/lms/static/coffee/src/instructor_dashboard/util.coffee +++ b/lms/static/coffee/src/instructor_dashboard/util.coffee @@ -43,7 +43,7 @@ create_task_list_table = ($table_tasks, tasks_data) -> id: 'task_type' field: 'task_type' name: 'Task Type' - minWidth: 100 + minWidth: 102 , id: 'task_input' field: 'task_input' diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index d1a04ce1850..2f8eb8947ed 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -26,6 +26,13 @@ @include font-size(16); } + .file-download-link a { + font-size: 15px; + color: $link-color; + text-decoration: underline; + padding: 5px; + } + // system feedback - messages .msg { border-radius: 1px; @@ -117,7 +124,7 @@ section.instructor-dashboard-content-2 { .slickgrid { margin-left: 1px; color:#333333; - font-size:11px; + font-size:12px; font-family: verdana,arial,sans-serif; .slick-header-column { @@ -428,13 +435,26 @@ section.instructor-dashboard-content-2 { line-height: 1.3em; } - .data-display { + .data-download-container { .data-display-table { .slickgrid { height: 400px; } } } + + .grades-download-container { + .grade-downloads-table { + .slickgrid { + height: 300px; + padding: 5px; + } + // Disable horizontal scroll bar when grid only has 1 column. Remove this CSS class when more columns added. + .slick-viewport { + overflow-x: hidden !important; + } + } + } } diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html index 7eb55fabfe8..cbbc2a871bb 100644 --- a/lms/templates/instructor/instructor_dashboard_2/data_download.html +++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html @@ -2,25 +2,46 @@ <%page args="section_data"/> -<h2>${_("Data Download")}</h2> +<div class="data-download-container action-type-container"> + <h2>${_("Data Download")}</h2> + <div class="request-response-error msg msg-error copy"></div> -<input type="button" name="list-profiles" value="${_("List enrolled students with profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"> -<input type="button" name="list-profiles" value="CSV" data-csv="true"> -<br> -<input type="button" name="dump-gradeconf" value="${_("Grading Configuration")}" data-endpoint="${ section_data['get_grading_config_url'] }"> -<input type="button" name="list-anon-ids" value="${_("Get Student Anonymized IDs CSV")}" data-csv="true" class="csv" data-endpoint="${ section_data['get_anon_ids_url'] }" class="${'is-disabled' if disable_buttons else ''}"> + <p>${_("The following button displays a list of all students enrolled in this course, along with profile information such as email address and username. The data can also be downloaded as a CSV file.")}</p> -<div class="data-display"> - <div class="data-display-text"></div> + <p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"> + <input type="button" name="list-profiles" value="${_("Download profile information as a CSV")}" data-csv="true"></p> <div class="data-display-table"></div> - <div class="request-response-error"></div> + + <br> + <p>${_("Displays 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><input type="button" name="dump-gradeconf" value="${_("Grading Configuration")}" data-endpoint="${ section_data['get_grading_config_url'] }"></p> + + <div class="data-display-text"></div> + <br> + + <p>${_("Download a CSV of anonymized student IDs by clicking this button.")}</p> + <p><input type="button" name="list-anon-ids" value="${_("Get Student Anonymized IDs CSV")}" data-csv="true" class="csv" data-endpoint="${ section_data['get_anon_ids_url'] }" class="${'is-disabled' if disable_buttons else ''}"></p> +</div> %if settings.MITX_FEATURES.get('ENABLE_S3_GRADE_DOWNLOADS'): - <div> - <h2> ${_("Grades")}</h2> - <input type="button" name="calculate-grades-csv" value="${_('Calculate Grades')}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/> - <br/> - <p>${_("Available grades downloads:")}</p> + <div class="grades-download-container action-type-container"> + <hr> + <h2> ${_("Grade Reports")}</h2> + + %if settings.MITX_FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']: + <p>${_("The following button will generate a CSV grade report for all currently enrolled students. For large courses, generating this report may take a few hours.")}</p> + + <p>${_("The report is generated in the background, meaning it is OK to navigate away from this page while your report is generating. Generated reports appear in a table below and can be downloaded.")}</p> + + <div class="request-response msg msg-confirm copy"></div> + <div class="request-response-error msg msg-warning copy"></div> + <br> + + <p><input type="button" name="calculate-grades-csv" value="${_("Generate Grade Report")}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/></p> + %endif + + <p><b>${_("Reports Available for Download")}</b></p> + <p>${_("File links are generated on demand and expire within 5 minutes due to the sensitive nature of student grade information. Please note that the report filename contains a timestamp that represents when your file was generated; this timestamp is UTC, not your local timezone.")}</p><br> <div class="grade-downloads-table" data-endpoint="${ section_data['list_grade_downloads_url'] }" ></div> </div> %endif @@ -34,4 +55,3 @@ <div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div> </div> %endif -</div> diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index 4d608100088..2762ba48990 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -5,7 +5,6 @@ <h2>${_("Student-specific grade inspection")}</h2> <div class="request-response-error"></div> <p> - <!-- Doesn't work for username but this MUST work --> ${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} <input type="text" name="student-select-progress" placeholder="${_("Student Email or Username")}"> </p> @@ -26,7 +25,6 @@ <h2>${_("Student-specific grade adjustment")}</h2> <div class="request-response-error"></div> <p> - <!-- Doesn't work for username but this MUST work --> ${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} <input type="text" name="student-select-grade" placeholder="${_("Student Email or Username")}"> </p> @@ -60,18 +58,18 @@ <p> %if section_data['access']['instructor']: <p> ${_('You may also delete the entire state of a student for the specified problem:')} </p> - <input type="button" class="molly-guard" name="delete-state-single" value="${_("Delete Student State for Problem")}" data-endpoint="${ section_data['reset_student_attempts_url'] }"> + <p><input type="button" class="molly-guard" name="delete-state-single" value="${_("Delete Student State for Problem")}" data-endpoint="${ section_data['reset_student_attempts_url'] }"></p> %endif </p> %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']: <p> - ${_("Rescoring runs in the background, and status for active tasks will appear in a table on the Course Info tab. " + ${_("Rescoring runs in the background, and status for active tasks will appear in the 'Pending Instructor Tasks' table. " "To see status for all tasks submitted for this problem and student, click on this button:")} </p> - <input type="button" name="task-history-single" value="${_("Show Background Task History for Student")}" data-endpoint="${ section_data['list_instructor_tasks_url'] }"> + <p><input type="button" name="task-history-single" value="${_("Show Background Task History for Student")}" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></p> <div class="task-history-single-table"></div> %endif <hr> @@ -101,10 +99,10 @@ </p> <p> <p> - ${_("These actions run in the background, and status for active tasks will appear in a table on the Course Info tab. " + ${_("The above actions run in the background, and status for active tasks will appear in a table on the Course Info tab. " "To see status for all tasks submitted for this problem, click on this button")}: </p> - <input type="button" name="task-history-all" value="${_("Show Background Task History for Problem")}" data-endpoint="${ section_data['list_instructor_tasks_url'] }"> + <p><input type="button" name="task-history-all" value="${_("Show Background Task History for Problem")}" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></p> <div class="task-history-all-table"></div> </p> </div> -- GitLab