Commit 0d101d8e by Braden MacDonald Committed by Jonathan Piacenti

Add CSV export to proof of concept

parent f57916c2
...@@ -23,7 +23,6 @@ Data Export: An XBlock for instructors to export student answers from a course. ...@@ -23,7 +23,6 @@ Data Export: An XBlock for instructors to export student answers from a course.
All processing is done offline. All processing is done offline.
""" """
import json import json
from .tasks import export_data as export_data_task
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, Dict from xblock.fields import Scope, String, Dict
from xblock.fragment import Fragment from xblock.fragment import Fragment
...@@ -71,6 +70,7 @@ class DataExportBlock(XBlock): ...@@ -71,6 +70,7 @@ class DataExportBlock(XBlock):
""" """
If we're waiting for an export, see if it has finished, and if so, get the result. If we're waiting for an export, see if it has finished, and if so, get the result.
""" """
from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
if self.active_export_task_id: if self.active_export_task_id:
async_result = export_data_task.AsyncResult(self.active_export_task_id) async_result = export_data_task.AsyncResult(self.active_export_task_id)
if async_result.ready(): if async_result.ready():
...@@ -96,12 +96,24 @@ class DataExportBlock(XBlock): ...@@ -96,12 +96,24 @@ class DataExportBlock(XBlock):
html = loader.render_template('templates/html/data_export.html', { html = loader.render_template('templates/html/data_export.html', {
'export_pending': bool(self.active_export_task_id), 'export_pending': bool(self.active_export_task_id),
'last_export_result': self.last_export_result, 'last_export_result': self.last_export_result,
'download_url': self.download_url_for_last_report,
}) })
fragment = Fragment(html) fragment = Fragment(html)
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
@property
def download_url_for_last_report(self):
""" Get the URL for the last report, if any """
# 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:
return None
from instructor_task.models import ReportStore
report_store = ReportStore.from_config()
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)
@XBlock.json_handler @XBlock.json_handler
def delete_export(self, request, suffix=''): def delete_export(self, request, suffix=''):
self._delete_export() self._delete_export()
...@@ -114,6 +126,7 @@ class DataExportBlock(XBlock): ...@@ -114,6 +126,7 @@ class DataExportBlock(XBlock):
@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 """
from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
# TODO: Verify instructor permissions # 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())
......
""" """
This file contains celery tasks for contentstore views This file contains celery tasks for contentstore views
""" """
import datetime
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
import datetime
from instructor_task.models import ReportStore
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
from .mcq import MCQBlock, RatingBlock
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
...@@ -21,23 +23,47 @@ def export_data(source_block_id_str, user_id): ...@@ -21,23 +23,47 @@ def export_data(source_block_id_str, user_id):
logger.debug("Beginning data export") logger.debug("Beginning data export")
block_key = UsageKey.from_string(source_block_id_str) block_key = UsageKey.from_string(source_block_id_str)
block = modulestore().get_item(block_key) src_block = modulestore().get_item(block_key)
course_key = src_block.scope_ids.usage_id.course_key
root = block # Get the root block:
root = src_block
while root.parent: while root.parent:
root = root.get_parent() root = root.get_parent()
course_name = root.display_name # 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)):
blocks_to_include.append(block)
elif block.has_children:
for child_id in block.children:
scan_for_blocks(block.runtime.get_block(child_id))
scan_for_blocks(root)
# Define the header rows of our CSV:
rows = []
rows.append(block.display_name_with_default for block in blocks_to_include)
rows.append(block.scope_ids.block_type for block in blocks_to_include)
rows.append(block.scope_ids.usage_id for block in blocks_to_include)
# Load the actual student submissions for each block in blocks_to_include.
# Note this requires one giant query per block (all student submissions for each block, one block at a time)
import time # Generate the CSV:
time.sleep(5) filename = u"pb-data-export-{}.csv".format(report_date.strftime("%Y-%m-%d-%H%M%S"))
report_store = ReportStore.from_config()
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()
logger.debug("Done data export - took {} seconds".format(generation_time_s)) logger.debug("Done data export - took {} seconds".format(generation_time_s))
return { return {
"error": None, "error": None,
"example": course_name, "report_filename": filename,
"report_date": report_date.isoformat(), "report_date": report_date.isoformat(),
"generation_time_s": generation_time_s, "generation_time_s": generation_time_s,
} }
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
{% else %} {% else %}
<p>Date: {{ last_export_result.report_date }}</p> <p>Date: {{ last_export_result.report_date }}</p>
<p>Took {{ last_export_result.generation_time_s }} seconds to generate.</p> <p>Took {{ last_export_result.generation_time_s }} seconds to generate.</p>
<p>Example of block traversal and data access: {{ last_export_result.example }}</p> <p>Download: <a href="{{download_url}}">{{ last_export_result.report_filename }}</a></p>
{% endif %} {% endif %}
<p><button class="delete-data-export">Delete export</button></p> <p><button class="delete-data-export">Delete export</button></p>
......
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