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):
"""
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
})
......
......@@ -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
......
......@@ -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
......
......@@ -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'
......
......@@ -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;
}
}
}
}
......
......@@ -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>
......@@ -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>
......
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