Commit e2423386 by Sarina Canelake

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
parent e0aa8cf7
...@@ -778,10 +778,12 @@ def calculate_grades_csv(request, course_id): ...@@ -778,10 +778,12 @@ def calculate_grades_csv(request, course_id):
""" """
try: try:
instructor_task.api.submit_calculate_grades_csv(request, course_id) 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: 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({ return JsonResponse({
"status" : "Grade calculation already running" "status": already_running_status
}) })
......
...@@ -196,6 +196,9 @@ MITX_FEATURES = { ...@@ -196,6 +196,9 @@ MITX_FEATURES = {
# Grade calculation started from the new instructor dashboard will write # Grade calculation started from the new instructor dashboard will write
# grades CSV files to S3 and give links for downloads. # grades CSV files to S3 and give links for downloads.
'ENABLE_S3_GRADE_DOWNLOADS' : True, '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 # Used for A/B testing
......
...@@ -17,17 +17,23 @@ class DataDownload ...@@ -17,17 +17,23 @@ class DataDownload
# this object to call event handlers like 'onClickTitle' # this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @ @$section.data 'wrapper', @
# gather elements # 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_studs_btn = @$section.find("input[name='list-profiles']'")
@$list_anon_btn = @$section.find("input[name='list-anon-ids']'") @$list_anon_btn = @$section.find("input[name='list-anon-ids']'")
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") @$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) @grade_downloads = new GradeDownloads(@$section)
@instructor_tasks = new (PendingInstructorTasks()) @$section @instructor_tasks = new (PendingInstructorTasks()) @$section
@clear_display()
# attach click handlers # attach click handlers
# The list-anon case is always CSV # The list-anon case is always CSV
...@@ -46,8 +52,9 @@ class DataDownload ...@@ -46,8 +52,9 @@ class DataDownload
url += '/csv' url += '/csv'
location.href = url location.href = url
else else
# Dynamically generate slickgrid table for displaying student profile information
@clear_display() @clear_display()
@$display_table.text 'Loading...' @$download_display_table.text gettext('Loading...')
# fetch user list # fetch user list
$.ajax $.ajax
...@@ -55,7 +62,7 @@ class DataDownload ...@@ -55,7 +62,7 @@ class DataDownload
url: url url: url
error: std_ajax_err => error: std_ajax_err =>
@clear_display() @clear_display()
@$request_response_error.text "Error getting student list." @$download_request_response_error.text gettext("Error getting student list.")
success: (data) => success: (data) =>
@clear_display() @clear_display()
...@@ -64,12 +71,13 @@ class DataDownload ...@@ -64,12 +71,13 @@ class DataDownload
enableCellNavigation: true enableCellNavigation: true
enableColumnReorder: false enableColumnReorder: false
forceFitColumns: true forceFitColumns: true
rowHeight: 35
columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features) columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features)
grid_data = data.students grid_data = data.students
$table_placeholder = $ '<div/>', class: 'slickgrid' $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 = new Slick.Grid($table_placeholder, grid_data, columns, options)
# grid.autosizeColumns() # grid.autosizeColumns()
...@@ -81,13 +89,31 @@ class DataDownload ...@@ -81,13 +89,31 @@ class DataDownload
url: url url: url
error: std_ajax_err => error: std_ajax_err =>
@clear_display() @clear_display()
@$request_response_error.text "Error getting grading configuration." @$download_request_response_error.text gettext("Error retrieving grading configuration.")
success: (data) => success: (data) =>
@clear_display() @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. # handler for when the section title is clicked.
onClickTitle: -> onClickTitle: ->
# Clear display of anything that was here before
@clear_display()
@instructor_tasks.task_poller.start() @instructor_tasks.task_poller.start()
@grade_downloads.downloads_poller.start() @grade_downloads.downloads_poller.start()
...@@ -97,36 +123,32 @@ class DataDownload ...@@ -97,36 +123,32 @@ class DataDownload
@grade_downloads.downloads_poller.stop() @grade_downloads.downloads_poller.stop()
clear_display: -> clear_display: ->
@$display_text.empty() # Clear any generated tables, warning messages, etc.
@$display_table.empty() @$download_display_text.empty()
@$request_response_error.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 class GradeDownloads
### Grade Downloads -- links expire quickly, so we refresh every 5 mins #### ### Grade Downloads -- links expire quickly, so we refresh every 5 mins ####
constructor: (@$section) -> 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' @$grades = @$section.find '.grades-download-container'
@$request_response_error = @$display.find '.request-response-error' @$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 POLL_INTERVAL = 1000 * 60 * 5 # 5 minutes in ms
@downloads_poller = new window.InstructorDashboard.util.IntervalManager( @downloads_poller = new window.InstructorDashboard.util.IntervalManager(
POLL_INTERVAL, => @reload_grade_downloads() 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: -> reload_grade_downloads: ->
endpoint = @$grade_downloads_table.data 'endpoint' endpoint = @$grade_downloads_table.data 'endpoint'
$.ajax $.ajax
...@@ -145,15 +167,17 @@ class GradeDownloads ...@@ -145,15 +167,17 @@ class GradeDownloads
options = options =
enableCellNavigation: true enableCellNavigation: true
enableColumnReorder: false enableColumnReorder: false
autoHeight: true rowHeight: 30
forceFitColumns: true forceFitColumns: true
columns = [ columns = [
id: 'link' id: 'link'
field: 'link' field: 'link'
name: 'File' name: gettext('File Name (Newest First)')
sortable: false, toolTip: gettext("Links are generated on demand and expire within 5 minutes due to the sensitive nature of student grade information.")
minWidth: 200, sortable: false
minWidth: 150
cssClass: "file-download-link"
formatter: (row, cell, value, columnDef, dataContext) -> formatter: (row, cell, value, columnDef, dataContext) ->
'<a href="' + dataContext['url'] + '">' + dataContext['name'] + '</a>' '<a href="' + dataContext['url'] + '">' + dataContext['name'] + '</a>'
] ]
...@@ -161,8 +185,7 @@ class GradeDownloads ...@@ -161,8 +185,7 @@ class GradeDownloads
$table_placeholder = $ '<div/>', class: 'slickgrid' $table_placeholder = $ '<div/>', class: 'slickgrid'
@$grade_downloads_table.append $table_placeholder @$grade_downloads_table.append $table_placeholder
grid = new Slick.Grid($table_placeholder, grade_downloads_data, columns, options) grid = new Slick.Grid($table_placeholder, grade_downloads_data, columns, options)
grid.autosizeColumns()
# export for use # export for use
......
...@@ -43,7 +43,7 @@ create_task_list_table = ($table_tasks, tasks_data) -> ...@@ -43,7 +43,7 @@ create_task_list_table = ($table_tasks, tasks_data) ->
id: 'task_type' id: 'task_type'
field: 'task_type' field: 'task_type'
name: 'Task Type' name: 'Task Type'
minWidth: 100 minWidth: 102
, ,
id: 'task_input' id: 'task_input'
field: 'task_input' field: 'task_input'
......
...@@ -26,6 +26,13 @@ ...@@ -26,6 +26,13 @@
@include font-size(16); @include font-size(16);
} }
.file-download-link a {
font-size: 15px;
color: $link-color;
text-decoration: underline;
padding: 5px;
}
// system feedback - messages // system feedback - messages
.msg { .msg {
border-radius: 1px; border-radius: 1px;
...@@ -117,7 +124,7 @@ section.instructor-dashboard-content-2 { ...@@ -117,7 +124,7 @@ section.instructor-dashboard-content-2 {
.slickgrid { .slickgrid {
margin-left: 1px; margin-left: 1px;
color:#333333; color:#333333;
font-size:11px; font-size:12px;
font-family: verdana,arial,sans-serif; font-family: verdana,arial,sans-serif;
.slick-header-column { .slick-header-column {
...@@ -428,13 +435,26 @@ section.instructor-dashboard-content-2 { ...@@ -428,13 +435,26 @@ section.instructor-dashboard-content-2 {
line-height: 1.3em; line-height: 1.3em;
} }
.data-display { .data-download-container {
.data-display-table { .data-display-table {
.slickgrid { .slickgrid {
height: 400px; 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;
}
}
}
} }
......
...@@ -2,25 +2,46 @@ ...@@ -2,25 +2,46 @@
<%page args="section_data"/> <%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'] }"> <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>
<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 ''}">
<div class="data-display"> <p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }">
<div class="data-display-text"></div> <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="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'): %if settings.MITX_FEATURES.get('ENABLE_S3_GRADE_DOWNLOADS'):
<div> <div class="grades-download-container action-type-container">
<h2> ${_("Grades")}</h2> <hr>
<input type="button" name="calculate-grades-csv" value="${_('Calculate Grades')}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/> <h2> ${_("Grade Reports")}</h2>
<br/>
<p>${_("Available grades downloads:")}</p> %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 class="grade-downloads-table" data-endpoint="${ section_data['list_grade_downloads_url'] }" ></div>
</div> </div>
%endif %endif
...@@ -34,4 +55,3 @@ ...@@ -34,4 +55,3 @@
<div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div> <div class="running-tasks-table" data-endpoint="${ section_data['list_instructor_tasks_url'] }"></div>
</div> </div>
%endif %endif
</div>
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
<h2>${_("Student-specific grade inspection")}</h2> <h2>${_("Student-specific grade inspection")}</h2>
<div class="request-response-error"></div> <div class="request-response-error"></div>
<p> <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)} ${_("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")}"> <input type="text" name="student-select-progress" placeholder="${_("Student Email or Username")}">
</p> </p>
...@@ -26,7 +25,6 @@ ...@@ -26,7 +25,6 @@
<h2>${_("Student-specific grade adjustment")}</h2> <h2>${_("Student-specific grade adjustment")}</h2>
<div class="request-response-error"></div> <div class="request-response-error"></div>
<p> <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)} ${_("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")}"> <input type="text" name="student-select-grade" placeholder="${_("Student Email or Username")}">
</p> </p>
...@@ -60,18 +58,18 @@ ...@@ -60,18 +58,18 @@
<p> <p>
%if section_data['access']['instructor']: %if section_data['access']['instructor']:
<p> ${_('You may also delete the entire state of a student for the specified problem:')} </p> <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 %endif
</p> </p>
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']: %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
<p> <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:")} "To see status for all tasks submitted for this problem and student, click on this button:")}
</p> </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> <div class="task-history-single-table"></div>
%endif %endif
<hr> <hr>
...@@ -101,10 +99,10 @@ ...@@ -101,10 +99,10 @@
</p> </p>
<p> <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")}: "To see status for all tasks submitted for this problem, click on this button")}:
</p> </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> <div class="task-history-all-table"></div>
</p> </p>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment