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. ...@@ -24,6 +24,7 @@ All processing is done offline.
""" """
import json import json
from xblock.core import XBlock from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, String, Dict from xblock.fields import Scope, String, Dict
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
...@@ -99,7 +100,12 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock): ...@@ -99,7 +100,12 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock):
""" Normal View """ """ Normal View """
if not self.user_is_staff(): if not self.user_is_staff():
return Fragment(u'<p>This interface can only be used by course staff.</p>') 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 = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/data_export.css')) 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')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/data_export.js'))
...@@ -126,12 +132,21 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock): ...@@ -126,12 +132,21 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock):
'download_url': self.download_url_for_last_report, '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 @XBlock.json_handler
def get_status(self, request, suffix=''): def get_status(self, data, suffix=''):
return self._get_status() return self._get_status()
@XBlock.json_handler @XBlock.json_handler
def delete_export(self, request, suffix=''): def delete_export(self, data, suffix=''):
self._delete_export() self._delete_export()
return self._get_status() return self._get_status()
...@@ -140,14 +155,32 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock): ...@@ -140,14 +155,32 @@ class DataExportBlock(SubmittingXBlockMixin, XBlock):
self.active_export_task_id = '' self.active_export_task_id = ''
@XBlock.json_handler @XBlock.json_handler
def start_export(self, request, suffix=''): def start_export(self, data, suffix=''):
""" Start a new asynchronous export """ """ 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(): if not self.user_is_staff():
return {'error': 'permission denied'} return {'error': 'permission denied'}
from .tasks import export_data as export_data_task # Import here since this is edX LMS specific from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
self._delete_export() 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( 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(): if async_result.ready():
# In development mode, the task may have executed synchronously. # In development mode, the task may have executed synchronously.
......
function DataExportBlock(runtime, element) { function DataExportBlock(runtime, element) {
'use strict'; 'use strict';
var $element = $(element);
// Set up gettext in case it isn't available in the client runtime: // Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") { if (typeof gettext == "undefined") {
window.gettext = function gettext_stub(string) { return string; }; window.gettext = function gettext_stub(string) { return string; };
window.ngettext = function ngettext_stub(strA, strB, n) { return n == 1 ? strA : strB; }; window.ngettext = function ngettext_stub(strA, strB, n) { return n == 1 ? strA : strB; };
} }
var $startButton = $('.data-export-start', element); var $startButton = $element.find('.data-export-start');
var $cancelButton = $('.data-export-cancel', element); var $cancelButton = $element.find('.data-export-cancel');
var $downloadButton = $('.data-export-download', element); var $downloadButton = $element.find('.data-export-download');
var $deleteButton = $('.data-export-delete', element); 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; var status;
function getStatus() { function getStatus() {
$.ajax({ $.ajax({
...@@ -19,6 +24,7 @@ function DataExportBlock(runtime, element) { ...@@ -19,6 +24,7 @@ function DataExportBlock(runtime, element) {
dataType: 'json' dataType: 'json'
}); });
} }
function updateStatus(newStatus) { function updateStatus(newStatus) {
var statusChanged = newStatus !== status; var statusChanged = newStatus !== status;
status = newStatus; status = newStatus;
...@@ -28,17 +34,25 @@ function DataExportBlock(runtime, element) { ...@@ -28,17 +34,25 @@ function DataExportBlock(runtime, element) {
} }
if (statusChanged) updateView(); if (statusChanged) updateView();
} }
function showSpinner() { function showSpinner() {
$startButton.prop('disabled', true); $startButton.prop('disabled', true);
$cancelButton.prop('disabled', true); $cancelButton.prop('disabled', true);
$downloadButton.prop('disabled', true); $downloadButton.prop('disabled', true);
$deleteButton.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') $('<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() { function updateView() {
var $statusArea = $('.data-export-status', element), startTime; var $statusArea = $('.data-export-status', $element), startTime;
$statusArea.empty(); $statusArea.empty();
$startButton.toggle(!status.export_pending).prop('disabled', false); $startButton.toggle(!status.export_pending).prop('disabled', false);
$cancelButton.toggle(status.export_pending).prop('disabled', false); $cancelButton.toggle(status.export_pending).prop('disabled', false);
...@@ -83,24 +97,40 @@ function DataExportBlock(runtime, element) { ...@@ -83,24 +97,40 @@ function DataExportBlock(runtime, element) {
} }
} }
} }
function addHandler($button, handlerName) {
function addHandler($button, handlerName, form_submit) {
$button.on('click', function() { $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({ $.ajax({
type: 'POST', type: 'POST',
url: runtime.handlerUrl(element, handlerName), url: runtime.handlerUrl(element, handlerName),
data: '{}', data: data,
success: updateStatus, success: updateStatus,
error: handleError,
dataType: 'json' dataType: 'json'
}); });
showSpinner(); showSpinner();
}); });
} }
addHandler($startButton, 'start_export');
addHandler($startButton, 'start_export', true);
addHandler($cancelButton, 'cancel_export'); addHandler($cancelButton, 'cancel_export');
addHandler($deleteButton, 'delete_export'); addHandler($deleteButton, 'delete_export');
$downloadButton.on('click', function() { $downloadButton.on('click', function() {
window.location.href = status.download_url; window.location.href = status.download_url;
}); });
showSpinner(); showSpinner();
getStatus(); getStatus();
} }
...@@ -6,6 +6,7 @@ import time ...@@ -6,6 +6,7 @@ import time
from celery.task import task from celery.task import task
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from instructor_task.models import ReportStore from instructor_task.models import ReportStore
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -17,7 +18,7 @@ logger = get_task_logger(__name__) ...@@ -17,7 +18,7 @@ logger = get_task_logger(__name__)
@task() @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. 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): ...@@ -25,22 +26,33 @@ def export_data(source_block_id_str, user_id=None):
logger.debug("Beginning data export") logger.debug("Beginning data export")
try:
block_key = UsageKey.from_string(source_block_id_str) block_key = UsageKey.from_string(source_block_id_str)
src_block = modulestore().get_item(block_key) src_block = modulestore().get_item(block_key)
course_key = src_block.scope_ids.usage_id.course_key.replace(branch=None, version_guid=None) 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) course_key_str = unicode(course_key)
# Get the root block:
root = src_block root = src_block
if get_root:
# Get the root block for the course.
while root.parent: while root.parent:
root = root.get_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 # Build an ordered list of blocks to include in the export - each block is a column in the CSV file
blocks_to_include = [] blocks_to_include = []
def scan_for_blocks(block): def scan_for_blocks(block):
""" Recursively scan the course tree for blocks of interest """ """ 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) blocks_to_include.append(block)
elif block.has_children: elif block.has_children:
for child_id in block.children: for child_id in block.children:
...@@ -61,7 +73,7 @@ def export_data(source_block_id_str, user_id=None): ...@@ -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: # 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_id = unicode(block.scope_ids.usage_id.replace(branch=None, version_guid=None))
block_type = block.scope_ids.block_type 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) submissions = sub_api.get_all_submissions(course_key_str, block_id, block_type)
else: else:
student_dict = { student_dict = {
......
...@@ -7,7 +7,37 @@ ...@@ -7,7 +7,37 @@
<div class="data-export-actions"> <div class="data-export-actions">
<button class="data-export-download">{% trans "Download result" %}</button> <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-cancel">{% trans "Cancel current export" %}</button>
<button class="data-export-delete">{% trans "Delete result" %}</button> <button class="data-export-delete">{% trans "Delete result" %}</button>
</div> </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