Commit f57916c2 by Braden MacDonald Committed by Jonathan Piacenti

Celery proof of concept for data export block

parent f42c7c57
...@@ -2,6 +2,7 @@ from .mentoring import MentoringBlock ...@@ -2,6 +2,7 @@ from .mentoring import MentoringBlock
from .answer import AnswerBlock, AnswerRecapBlock from .answer import AnswerBlock, AnswerRecapBlock
from .choice import ChoiceBlock from .choice import ChoiceBlock
from .dashboard import DashboardBlock from .dashboard import DashboardBlock
from .data_export import DataExportBlock
from .mcq import MCQBlock, RatingBlock from .mcq import MCQBlock, RatingBlock
from .mrq import MRQBlock from .mrq import MRQBlock
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
......
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
"""
Data Export: An XBlock for instructors to export student answers from a course.
All processing is done offline.
"""
import json
from .tasks import export_data as export_data_task
from xblock.core import XBlock
from xblock.fields import Scope, String, Dict
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
loader = ResourceLoader(__name__)
@XBlock.wants('user')
class DataExportBlock(XBlock):
"""
DataExportBlock: An XBlock for instructors to export student answers from a course.
All processing is done offline.
"""
active_export_task_id = String(
# The UUID of the celery AsyncResult for the most recent export,
# IF we are sill waiting for it to finish
default="",
scope=Scope.user_state,
)
last_export_result = Dict(
# The info dict returned by the most recent successful export.
# If the export failed, it will have an "error" key set.
default=None,
scope=Scope.user_state,
)
has_author_view = True
@property
def display_name_with_default(self):
return "Data Export"
def author_view(self, context=None):
""" Studio View """
# 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)
return Fragment(u"<p>Data Export Block</p><p>This block only works from the LMS.</p>")
def studio_view(self, context=None):
""" 'Edit' form view in Studio """
return Fragment(u"<p>This block has no configuration options.</p>")
def check_pending_export(self):
"""
If we're waiting for an export, see if it has finished, and if so, get the result.
"""
if self.active_export_task_id:
async_result = export_data_task.AsyncResult(self.active_export_task_id)
if async_result.ready():
self._save_result(async_result)
def _save_result(self, task_result):
""" Given an AsyncResult or EagerResult, save it. """
self.active_export_task_id = ""
if task_result.successful():
if isinstance(task_result.result, dict) and not task_result.result.get('error'):
self.last_export_result = task_result.result
else:
self.last_export_result = {"error": u"Unexpected result: {}".format(repr(task_result.result))}
else:
self.last_export_result = {"error": unicode(task_result.result)}
def student_view(self, context=None):
""" Normal View """
# TODO: Verify instructor permissions
# Check if any pending export has finished:
self.check_pending_export()
# 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,
})
fragment = Fragment(html)
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/data_export.js'))
fragment.initialize_js('DataExportBlock')
return fragment
@XBlock.json_handler
def delete_export(self, request, suffix=''):
self._delete_export()
return {"result": "ok"}
def _delete_export(self):
self.last_export_result = None
self.active_export_task_id = ""
@XBlock.json_handler
def start_export(self, request, suffix=''):
""" Start a new asynchronous export """
# TODO: Verify instructor permissions
self._delete_export()
async_result = export_data_task.delay(unicode(self.scope_ids.usage_id), self.get_user_id())
if async_result.ready():
# In development mode, the task may have executed synchronously.
# Store the result now, because we won't be able to retrieve it later :-/
if async_result.successful():
# Make sure the result can be represented as JSON, since the non-eager celery
# requires that
json.dumps(async_result.result)
self._save_result(async_result)
else:
# The task is running asynchronously. Store the result ID so we can query its progress:
self.active_export_task_id = async_result.id
return {"result": "started"}
def get_user_id(self):
"""
Get the ID of the current user.
"""
user_service = self.runtime.service(self, 'user')
if user_service:
# May be None when creating bok choy test fixtures
user_id = user_service.get_current_user().opt_attrs.get('edx-platform.user_id', None)
else:
user_id = None
return user_id
function DataExportBlock(runtime, element) {
"use strict";
var $startExportBtn = $(".new-data-export", element);
var $deleteExportBtn = $(".delete-data-export", element);
$startExportBtn.on("click", function() {
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'start_export'),
data: JSON.stringify({}),
success: function(data) {
console.log("Success");
console.log(data);
},
dataType: "json",
});
});
$deleteExportBtn.on("click", function() {
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'delete_export'),
data: "{}",
success: function(data) {
$deleteExportBtn.prop('disabled', true);
},
dataType: "json",
});
});
}
"""
This file contains celery tasks for contentstore views
"""
import datetime
from celery.task import task
from celery.utils.log import get_task_logger
from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore
logger = get_task_logger(__name__)
@task()
def export_data(source_block_id_str, user_id):
"""
Reruns a course in a new celery task.
"""
report_date = datetime.datetime.now()
logger.debug("Beginning data export")
block_key = UsageKey.from_string(source_block_id_str)
block = modulestore().get_item(block_key)
root = block
while root.parent:
root = root.get_parent()
course_name = root.display_name
import time
time.sleep(5)
generation_time_s = (datetime.datetime.now() - report_date).total_seconds()
logger.debug("Done data export - took {} seconds".format(generation_time_s))
return {
"error": None,
"example": course_name,
"report_date": report_date.isoformat(),
"generation_time_s": generation_time_s,
}
<h3>Data Export</h3>
<script>
</script>
<p><button class="new-data-export">Start a new export</button></p>
{% if last_export_result %}
<h3>Export</h3>
{% if last_export_result.error %}
<p>Export failed! Reason: {{ last_export_result.error }}.</p>
{% else %}
<p>Date: {{ last_export_result.report_date }}</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>
{% endif %}
<p><button class="delete-data-export">Delete export</button></p>
{% elif export_pending %}
<p>Report is currently being generated...</p>
{% endif %}
...@@ -54,23 +54,7 @@ BLOCKS = [ ...@@ -54,23 +54,7 @@ BLOCKS = [
'pb-choice = problem_builder:ChoiceBlock', 'pb-choice = problem_builder:ChoiceBlock',
'pb-dashboard = problem_builder:DashboardBlock', 'pb-dashboard = problem_builder:DashboardBlock',
# Deprecated. You can temporarily uncomment and run 'python setup.py develop' if you have these blocks 'pb-data-export = problem_builder:DataExportBlock',
# installed from testing mentoring v2 and need to get past an error message.
#'mentoring = problem_builder:MentoringBlock', # Deprecated alias for problem-builder
#'answer = problem_builder:AnswerBlock',
#'mentoring-answer = problem_builder:AnswerBlock',
#'answer-recap = problem_builder:AnswerRecapBlock',
#'mentoring-answer-recap = problem_builder:AnswerRecapBlock',
#'mcq = problem_builder:MCQBlock',
#'mentoring-mcq = problem_builder:MCQBlock',
#'rating = problem_builder:RatingBlock',
#'mentoring-rating = problem_builder:RatingBlock',
#'mrq = problem_builder:MRQBlock',
#'mentoring-mrq = problem_builder:MRQBlock',
#'tip = problem_builder:TipBlock',
#'mentoring-tip = problem_builder:TipBlock',
#'choice = problem_builder:ChoiceBlock',
#'mentoring-choice = problem_builder:ChoiceBlock',
] ]
setup( setup(
......
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