Commit c338a1e2 by Jonathan Piacenti

Filter results by block usage id, student, or block type.

parent 47bb71c1
......@@ -24,6 +24,7 @@ All processing is done offline.
"""
import json
from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, String, Dict
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
......@@ -99,7 +100,12 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock):
""" Normal View """
if not self.user_is_staff():
return Fragment(u'<p>This interface can only be used by course staff.</p>')
html = loader.render_template('templates/html/data_export.html')
block_choices = {
_('Multiple Choice Question'): 'MCQBlock',
_('Rating Question'): 'RatingBlock',
_('Long Answer'): 'AnswerBlock',
}
html = loader.render_template('templates/html/data_export.html', {'block_choices': block_choices})
fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/data_export.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/data_export.js'))
......@@ -126,12 +132,21 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock):
'download_url': self.download_url_for_last_report,
}
def raise_error(self, code, message):
"""
Raises an error and marks the block with a simulated failed task dict.
"""
self.last_export_result = {
'error': message,
}
raise JsonHandlerError(code, message)
@XBlock.json_handler
def get_status(self, request, suffix=''):
def get_status(self, data, suffix=''):
return self._get_status()
@XBlock.json_handler
def delete_export(self, request, suffix=''):
def delete_export(self, data, suffix=''):
self._delete_export()
return self._get_status()
......@@ -140,14 +155,32 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock):
self.active_export_task_id = ''
@XBlock.json_handler
def start_export(self, request, suffix=''):
def start_export(self, data, suffix=''):
""" Start a new asynchronous export """
block_types = data.get('block_types', None)
username = data.get('username', None)
root_block_id = data.get('root_block_id', None)
if not root_block_id:
root_block_id = unicode(self.scope_ids.usage_id)
get_root = True
else:
self.runtime.course_id.make_usage_key(root_block_id)
get_root = False
print root_block_id
user_service = self.runtime.service(self, 'user')
if not self.user_is_staff():
return {'error': 'permission denied'}
from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
self._delete_export()
if not username:
user_id = None
else:
user_id = user_service.get_anonymous_user_id(username, unicode(self.runtime.course_id))
if user_id is None:
self.raise_error(404, _("Could not find the specified username."))
async_result = export_data_task.delay(
unicode(self.scope_ids.usage_id),
root_block_id, block_types, user_id, get_root=get_root,
)
if async_result.ready():
# In development mode, the task may have executed synchronously.
......
function DataExportBlock(runtime, element) {
'use strict';
var $element = $(element);
// Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") {
window.gettext = function gettext_stub(string) { return string; };
window.ngettext = function ngettext_stub(strA, strB, n) { return n == 1 ? strA : strB; };
}
var $startButton = $('.data-export-start', element);
var $cancelButton = $('.data-export-cancel', element);
var $downloadButton = $('.data-export-download', element);
var $deleteButton = $('.data-export-delete', element);
var $startButton = $element.find('.data-export-start');
var $cancelButton = $element.find('.data-export-cancel');
var $downloadButton = $element.find('.data-export-download');
var $deleteButton = $element.find('.data-export-delete');
var $blockTypes = $element.find("select[name='block_types']");
var $rootBlockIds = $element.find("input[name='root_block_id']");
var $username = $element.find("input[name='username']");
var status;
function getStatus() {
$.ajax({
......@@ -19,6 +24,7 @@ function DataExportBlock(runtime, element) {
dataType: 'json'
});
}
function updateStatus(newStatus) {
var statusChanged = newStatus !== status;
status = newStatus;
......@@ -28,17 +34,25 @@ function DataExportBlock(runtime, element) {
}
if (statusChanged) updateView();
}
function showSpinner() {
$startButton.prop('disabled', true);
$cancelButton.prop('disabled', true);
$downloadButton.prop('disabled', true);
$deleteButton.prop('disabled', true);
$('.data-export-status', element).empty().append(
$('.data-export-status', $element).empty().append(
$('<i>').addClass('icon fa fa-spinner fa-spin')
);
}
function handleError(data) {
// Shim to make the XBlock JsonHandlerError response work with our format.
status = {'last_export_result': JSON.parse(data.responseText), 'export_pending': false};
updateView();
}
function updateView() {
var $statusArea = $('.data-export-status', element), startTime;
var $statusArea = $('.data-export-status', $element), startTime;
$statusArea.empty();
$startButton.toggle(!status.export_pending).prop('disabled', false);
$cancelButton.toggle(status.export_pending).prop('disabled', false);
......@@ -83,24 +97,40 @@ function DataExportBlock(runtime, element) {
}
}
}
function addHandler($button, handlerName) {
function addHandler($button, handlerName, form_submit) {
$button.on('click', function() {
var data;
if (form_submit) {
data = {};
data['block_types'] = $blockTypes.val();
data['block_types'] = $blockTypes.val();
data['root_block_id'] = $rootBlockIds.val();
data['username'] = $username.val();
data = JSON.stringify(data);
} else {
data = '{}';
}
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, handlerName),
data: '{}',
data: data,
success: updateStatus,
error: handleError,
dataType: 'json'
});
showSpinner();
});
}
addHandler($startButton, 'start_export');
addHandler($startButton, 'start_export', true);
addHandler($cancelButton, 'cancel_export');
addHandler($deleteButton, 'delete_export');
$downloadButton.on('click', function() {
window.location.href = status.download_url;
});
showSpinner();
getStatus();
}
......@@ -6,6 +6,7 @@ import time
from celery.task import task
from celery.utils.log import get_task_logger
from instructor_task.models import ReportStore
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore
......@@ -17,7 +18,7 @@ logger = get_task_logger(__name__)
@task()
def export_data(source_block_id_str, user_id=None):
def export_data(source_block_id_str, block_types, user_id, get_root=True):
"""
Exports student answers to all MCQ questions to a CSV file.
"""
......@@ -25,22 +26,33 @@ def export_data(source_block_id_str, user_id=None):
logger.debug("Beginning data export")
block_key = UsageKey.from_string(source_block_id_str)
src_block = modulestore().get_item(block_key)
course_key = src_block.scope_ids.usage_id.course_key.replace(branch=None, version_guid=None)
try:
block_key = UsageKey.from_string(source_block_id_str)
src_block = modulestore().get_item(block_key)
course_key = src_block.scope_ids.usage_id.course_key.replace(branch=None, version_guid=None)
except InvalidKeyError:
raise ValueError("Could not find the specified Block ID.")
course_key_str = unicode(course_key)
# Get the root block:
root = src_block
while root.parent:
root = root.get_parent()
if get_root:
# Get the root block for the course.
while root.parent:
root = root.get_parent()
type_map = {cls.__name__: cls for cls in [MCQBlock, RatingBlock, AnswerBlock]}
if not block_types:
block_types = tuple(type_map.values())
else:
block_types = tuple([type_map[class_name] for class_name in block_types])
# Build an ordered list of blocks to include in the export - each block is a column in the CSV file
blocks_to_include = []
def scan_for_blocks(block):
""" Recursively scan the course tree for blocks of interest """
if isinstance(block, (MCQBlock, RatingBlock, AnswerBlock)):
if isinstance(block, block_types):
blocks_to_include.append(block)
elif block.has_children:
for child_id in block.children:
......@@ -61,7 +73,7 @@ def export_data(source_block_id_str, user_id=None):
# Get all of the most recent student submissions for this block:
block_id = unicode(block.scope_ids.usage_id.replace(branch=None, version_guid=None))
block_type = block.scope_ids.block_type
if user_id is None:
if not user_id:
submissions = sub_api.get_all_submissions(course_key_str, block_id, block_type)
else:
student_dict = {
......
......@@ -7,7 +7,37 @@
<div class="data-export-actions">
<button class="data-export-download">{% trans "Download result" %}</button>
<button class="data-export-start">{% trans "Start a new export" %}</button>
<button class="data-export-cancel">{% trans "Cancel current export" %}</button>
<button class="data-export-delete">{% trans "Delete result" %}</button>
</div>
<div class="export-options">
<div>
<label><input type="checkbox" name="filter_blocks" />{% trans "Filter by block type" %}</label>
</div>
<div>
<label>
{% trans "Select which blocks to filter by (selecting none will grab all types):" %}
<select multiple name="block_types">
{% for label, value in block_choices.items %}
<option value="{{value}}">{{label}}</option>
{% endfor %}
</select>
</label>
</div>
<div>
<label>
{% trans "Input the Usage ID of a chapter, section, or unit if you wish to only get results under it. Otherwise, it will grab all results for the course." %}
<input type="text" name="root_block_id" />
</label>
</div>
<div>
<label>
{% trans "Input the username of a student if you wish to query for a specific one. Otherwise, it will grab all results for all students." %}
<input type="text" name="username" />
</label>
</div>
</div>
<div class="data-export-actions">
<button class="data-export-start">{% trans "Start a new export" %}</button>
</div>
\ No newline at end of file
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