Commit f1d6fab2 by Sven Marnach Committed by Jonathan Piacenti

Adapt code to API changes in edx-platform and edx-submissions.

parent 06b7460b
...@@ -60,11 +60,11 @@ class DataExportBlock(XBlock): ...@@ -60,11 +60,11 @@ class DataExportBlock(XBlock):
""" Studio View """ """ Studio View """
# Warn the user that this block will only work from the LMS. (Since the CMS uses # Warn the user that this block will only work from the LMS. (Since the CMS uses
# different celery queues; our task listener is waiting for tasks on the LMS queue) # different celery queues; our task listener is waiting for tasks on the LMS queue)
return Fragment(u"<p>Data Export Block</p><p>This block only works from the LMS.</p>") return Fragment(u'<p>Data Export Block</p><p>This block only works from the LMS.</p>')
def studio_view(self, context=None): def studio_view(self, context=None):
""" 'Edit' form view in Studio """ """ 'Edit' form view in Studio """
return Fragment(u"<p>This block has no configuration options.</p>") return Fragment(u'<p>This block has no configuration options.</p>')
def check_pending_export(self): def check_pending_export(self):
""" """
...@@ -78,27 +78,22 @@ class DataExportBlock(XBlock): ...@@ -78,27 +78,22 @@ class DataExportBlock(XBlock):
def _save_result(self, task_result): def _save_result(self, task_result):
""" Given an AsyncResult or EagerResult, save it. """ """ Given an AsyncResult or EagerResult, save it. """
self.active_export_task_id = "" self.active_export_task_id = ''
if task_result.successful(): if task_result.successful():
if isinstance(task_result.result, dict) and not task_result.result.get('error'): if isinstance(task_result.result, dict) and not task_result.result.get('error'):
self.last_export_result = task_result.result self.last_export_result = task_result.result
else: else:
self.last_export_result = {"error": u"Unexpected result: {}".format(repr(task_result.result))} self.last_export_result = {'error': u'Unexpected result: {}'.format(repr(task_result.result))}
else: else:
self.last_export_result = {"error": unicode(task_result.result)} self.last_export_result = {'error': unicode(task_result.result)}
def student_view(self, context=None): def student_view(self, context=None):
""" Normal View """ """ Normal View """
# TODO: Verify instructor permissions if not self.user_is_staff():
# Check if any pending export has finished: return Fragment(u'<p>This interface can only be used by course staff.</p>')
self.check_pending_export() html = loader.render_template('templates/html/data_export.html')
# Render our HTML:
html = loader.render_template('templates/html/data_export.html', {
'export_pending': bool(self.active_export_task_id),
'last_export_result': self.last_export_result,
'download_url': self.download_url_for_last_report,
})
fragment = Fragment(html) 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')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/data_export.js'))
fragment.initialize_js('DataExportBlock') fragment.initialize_js('DataExportBlock')
return fragment return fragment
...@@ -107,27 +102,40 @@ class DataExportBlock(XBlock): ...@@ -107,27 +102,40 @@ class DataExportBlock(XBlock):
def download_url_for_last_report(self): def download_url_for_last_report(self):
""" Get the URL for the last report, if any """ """ Get the URL for the last report, if any """
# Unfortunately this is a bit inefficient due to the ReportStore API # Unfortunately this is a bit inefficient due to the ReportStore API
if not self.last_export_result or self.last_export_result["error"] is not None: if not self.last_export_result or self.last_export_result['error'] is not None:
return None return None
from instructor_task.models import ReportStore from instructor_task.models import ReportStore
report_store = ReportStore.from_config() report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
course_key = self.scope_ids.usage_id.course_key course_key = self.scope_ids.usage_id.course_key
return dict(report_store.links_for(course_key)).get(self.last_export_result["report_filename"], None) return dict(report_store.links_for(course_key)).get(self.last_export_result['report_filename'])
def _get_status(self):
self.check_pending_export()
return {
'export_pending': bool(self.active_export_task_id),
'last_export_result': self.last_export_result,
'download_url': self.download_url_for_last_report,
}
@XBlock.json_handler
def get_status(self, request, suffix=''):
return self._get_status()
@XBlock.json_handler @XBlock.json_handler
def delete_export(self, request, suffix=''): def delete_export(self, request, suffix=''):
self._delete_export() self._delete_export()
return {"result": "ok"} return self._get_status()
def _delete_export(self): def _delete_export(self):
self.last_export_result = None self.last_export_result = None
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, request, suffix=''):
""" Start a new asynchronous export """ """ Start a new asynchronous export """
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 from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
# TODO: Verify instructor permissions
self._delete_export() self._delete_export()
async_result = export_data_task.delay(unicode(self.scope_ids.usage_id), self.get_user_id()) async_result = export_data_task.delay(unicode(self.scope_ids.usage_id), self.get_user_id())
if async_result.ready(): if async_result.ready():
...@@ -141,16 +149,29 @@ class DataExportBlock(XBlock): ...@@ -141,16 +149,29 @@ class DataExportBlock(XBlock):
else: else:
# The task is running asynchronously. Store the result ID so we can query its progress: # The task is running asynchronously. Store the result ID so we can query its progress:
self.active_export_task_id = async_result.id self.active_export_task_id = async_result.id
return {"result": "started"} return self._get_status()
return {'result': 'started'}
def get_user_id(self): @XBlock.json_handler
""" def cancel_export(self, request, suffix=''):
Get the ID of the current user. from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
""" if self.active_export_task_id:
async_result = export_data_task.AsyncResult(self.active_export_task_id)
async_result.revoke()
self._delete_export()
def _get_user_attr(self, attr):
"""Get an attribute of the current user."""
user_service = self.runtime.service(self, 'user') user_service = self.runtime.service(self, 'user')
if user_service: if user_service:
# May be None when creating bok choy test fixtures # May be None when creating bok choy test fixtures
user_id = user_service.get_current_user().opt_attrs.get('edx-platform.user_id', None) return user_service.get_current_user().opt_attrs.get(attr)
else: return None
user_id = None
return user_id def user_is_staff(self):
"""Return a Boolean value indicating whether the current user is a member of staff."""
return self._get_user_attr('edx-platform.user_is_staff')
def get_user_id(self):
"""Get the edx-platform user_id of the current user."""
return self._get_user_attr('edx-platform.user_id')
.data-export-actions button {
display: none;
}
.data-export-status {
height: 8em;
}
function DataExportBlock(runtime, element) { function DataExportBlock(runtime, element) {
"use strict"; "use strict";
var $startExportBtn = $(".new-data-export", element); var $startButton = $(".data-export-start", element);
var $deleteExportBtn = $(".delete-data-export", element); var $cancelButton = $(".data-export-cancel", element);
$startExportBtn.on("click", function() { var $downloadButton = $(".data-export-download", element);
var $deleteButton = $(".data-export-delete", element);
var status;
function getStatus() {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: runtime.handlerUrl(element, 'start_export'), url: runtime.handlerUrl(element, 'get_status'),
data: JSON.stringify({}), data: "{}",
success: function(data) { success: updateStatus,
console.log("Success");
console.log(data);
},
dataType: "json", dataType: "json",
}); });
}); }
$deleteExportBtn.on("click", function() { function updateStatus(newStatus) {
var statusChanged = newStatus !== status;
status = newStatus;
if (status.export_pending) {
// Keep polling for status updates when an export is running.
setTimeout(getStatus, 1000);
}
if (statusChanged) updateView();
}
function updateView() {
var $statusArea = $(".data-export-status", element);
$statusArea.empty();
$startButton.toggle(!status.export_pending);
$cancelButton.toggle(status.export_pending);
$downloadButton.toggle(Boolean(status.download_url));
$deleteButton.toggle(Boolean(status.last_export_result));
if (status.last_export_result) {
if (status.last_export_result.error) {
$statusArea.append($('<p>').text(
'Data export failed. Reason: ' + status.last_export_result.error
));
} else {
$statusArea.append($('<p>').text(
'Date completed: ' + status.last_export_result.report_date
));
$statusArea.append($('<p>').text(
'The report took ' + status.last_export_result.generation_time_s +
' seconds to generate.'
));
}
} else {
if (status.export_pending) {
$statusArea.append($('<p>').text(
'The report is currently being generated…'
));
} else {
$statusArea.append($('<p>').text(
'No report data available.'
));
}
}
}
function addHandler($button, handlerName) {
$button.on("click", function() {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: runtime.handlerUrl(element, 'delete_export'), url: runtime.handlerUrl(element, handlerName),
data: "{}", data: "{}",
success: function(data) { success: updateStatus,
$deleteExportBtn.prop('disabled', true);
},
dataType: "json", dataType: "json",
}); });
}); });
}
addHandler($startButton, 'start_export');
addHandler($cancelButton, 'cancel_export');
addHandler($deleteButton, 'delete_export');
$downloadButton.on("click", function() {
window.location.href = status.download_url;
});
getStatus();
} }
...@@ -60,9 +60,10 @@ def export_data(source_block_id_str, user_id): ...@@ -60,9 +60,10 @@ def export_data(source_block_id_str, user_id):
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
for submission in sub_api.get_all_submissions(course_key_str, block_id, block_type): for submission in sub_api.get_all_submissions(course_key_str, block_id, block_type):
if submission.student_id not in student_submissions: student_id = submission['student_id']
student_submissions[submission.student_id] = [submission.student_id] + [""] * len(blocks_to_include) if student_id not in student_submissions:
student_submissions[submission.student_id][idx] = submission.answer student_submissions[student_id] = [student_id] + [""] * len(blocks_to_include)
student_submissions[student_id][idx] = submission['answer']
# Now change from a dict to an array ordered by student ID as we generate the remaining rows: # Now change from a dict to an array ordered by student ID as we generate the remaining rows:
for student_id in sorted(student_submissions.iterkeys()): for student_id in sorted(student_submissions.iterkeys()):
...@@ -71,7 +72,7 @@ def export_data(source_block_id_str, user_id): ...@@ -71,7 +72,7 @@ def export_data(source_block_id_str, user_id):
# Generate the CSV: # Generate the CSV:
filename = u"pb-data-export-{}.csv".format(report_date.strftime("%Y-%m-%d-%H%M%S")) filename = u"pb-data-export-{}.csv".format(report_date.strftime("%Y-%m-%d-%H%M%S"))
report_store = ReportStore.from_config() report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
report_store.store_rows(course_key, filename, rows) report_store.store_rows(course_key, filename, rows)
generation_time_s = (datetime.datetime.now() - report_date).total_seconds() generation_time_s = (datetime.datetime.now() - report_date).total_seconds()
......
<h3>Data Export</h3> <h3>Data Export</h3>
<script> <p>You can export all student answers to multiple-choice questions to a CSV file here.</p>
</script> <div class="data-export-status"></div>
<p><button class="new-data-export">Start a new export</button></p> <div class="data-export-actions">
<button class="data-export-download">Download result</button>
{% if last_export_result %} <button class="data-export-start">Start a new export</button>
<h3>Export</h3> <button class="data-export-cancel">Cancel current export</button>
{% if last_export_result.error %} <button class="data-export-delete">Delete result</button>
<p>Export failed! Reason: {{ last_export_result.error }}.</p> </div>
{% else %}
<p>Date: {{ last_export_result.report_date }}</p>
<p>Took {{ last_export_result.generation_time_s }} seconds to generate.</p>
<p>Download: <a href="{{download_url}}">{{ last_export_result.report_filename }}</a></p>
{% endif %}
<p><button class="delete-data-export">Delete export</button></p>
{% elif export_pending %}
<p>Report is currently being generated...</p>
{% endif %}
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