Commit ddc822aa by Jonathan Piacenti

Allow student access, answer submissions, and internationalization to data-export.

parent 1efe0ad9
......@@ -31,6 +31,7 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from problem_builder.sub_api import SubmittingXBlockMixin, sub_api
from .step import StepMixin
import uuid
......@@ -102,7 +103,7 @@ class AnswerMixin(object):
@XBlock.needs("i18n")
class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
"""
A field where the student enters an answer
......@@ -199,6 +200,11 @@ class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
"""
self.student_input = submission[0]['value'].strip()
self.save()
if sub_api:
# Also send to the submissions API:
sub_api.create_submission(self.student_item_key, self.student_input)
log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input))
return self.get_results()
......
......@@ -27,17 +27,29 @@ from xblock.core import XBlock
from xblock.fields import Scope, String, Dict
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from problem_builder.sub_api import SubmittingXBlockMixin
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
@XBlock.needs("i18n")
@XBlock.wants('user')
class DataExportBlock(XBlock):
class DataExportBlock(SubmittingXBlockMixin, XBlock):
"""
DataExportBlock: An XBlock for instructors to export student answers from a course.
All processing is done offline.
"""
display_name = String(
display_name=_("Title (Display name)"),
help=_("Title to display"),
default=_("Data Export"),
scope=Scope.settings
)
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
......@@ -62,10 +74,6 @@ class DataExportBlock(XBlock):
# 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.
......@@ -95,6 +103,7 @@ class DataExportBlock(XBlock):
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/vendor/underscore-min.js'))
fragment.initialize_js('DataExportBlock')
return fragment
......@@ -137,7 +146,9 @@ class DataExportBlock(XBlock):
return {'error': 'permission denied'}
from .tasks import export_data as export_data_task # Import here since this is edX LMS specific
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),
)
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 :-/
......@@ -150,7 +161,6 @@ class DataExportBlock(XBlock):
# The task is running asynchronously. Store the result ID so we can query its progress:
self.active_export_task_id = async_result.id
return self._get_status()
return {'result': 'started'}
@XBlock.json_handler
def cancel_export(self, request, suffix=''):
......@@ -171,7 +181,3 @@ class DataExportBlock(XBlock):
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')
function DataExportBlock(runtime, element) {
'use strict';
// 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);
......@@ -11,7 +16,7 @@ function DataExportBlock(runtime, element) {
url: runtime.handlerUrl(element, 'get_status'),
data: '{}',
success: updateStatus,
dataType: 'json',
dataType: 'json'
});
}
function updateStatus(newStatus) {
......@@ -42,27 +47,38 @@ function DataExportBlock(runtime, element) {
if (status.last_export_result) {
if (status.last_export_result.error) {
$statusArea.append($('<p>').text(
'Data export failed. Reason: ' + status.last_export_result.error
_.template(
gettext('Data export failed. Reason: <%= error %>'),
{'error': status.last_export_result.error}
)
));
} else {
startTime = new Date(status.last_export_result.start_timestamp * 1000);
$statusArea.append($('<p>').text(
'A report is available for download.'
gettext('A report is available for download.')
));
$statusArea.append($('<p>').text(
'It was created at ' + startTime.toString() +
' and took ' + status.last_export_result.generation_time_s.toFixed(1) +
' seconds to finish.'
_.template(
ngettext(
'It was created at <%= creation_time %> and took <%= seconds %> second to finish.',
'It was created at <%= creation_time %> and took <%= seconds %> seconds to finish.',
status.last_export_result.generation_time_s.toFixed(1)
),
{
'creation_time': startTime.toString(),
'seconds': status.last_export_result.generation_time_s.toFixed(1)
}
)
));
}
} else {
if (status.export_pending) {
$statusArea.append($('<p>').text(
'The report is currently being generated…'
gettext('The report is currently being generated…')
));
} else {
$statusArea.append($('<p>').text(
'No report data available.'
gettext('No report data available.')
));
}
}
......@@ -74,7 +90,7 @@ function DataExportBlock(runtime, element) {
url: runtime.handlerUrl(element, handlerName),
data: '{}',
success: updateStatus,
dataType: 'json',
dataType: 'json'
});
showSpinner();
});
......
......@@ -10,13 +10,14 @@ from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore
from .mcq import MCQBlock, RatingBlock
from problem_builder import AnswerBlock
from .sub_api import sub_api
logger = get_task_logger(__name__)
@task()
def export_data(source_block_id_str, user_id):
def export_data(source_block_id_str, user_id=None):
"""
Exports student answers to all MCQ questions to a CSV file.
"""
......@@ -39,7 +40,7 @@ def export_data(source_block_id_str, user_id):
def scan_for_blocks(block):
""" Recursively scan the course tree for blocks of interest """
if isinstance(block, (MCQBlock, RatingBlock)):
if isinstance(block, (MCQBlock, RatingBlock, AnswerBlock)):
blocks_to_include.append(block)
elif block.has_children:
for child_id in block.children:
......@@ -56,12 +57,23 @@ def export_data(source_block_id_str, user_id):
# 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)
student_submissions = {} # Key is student ID, value is a list with same length as blocks_to_include
for idx, block in enumerate(blocks_to_include, start=1): # start=1 since first column is stuent ID
for idx, block in enumerate(blocks_to_include, start=1): # start=1 since first column is student ID
# 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
for submission in sub_api.get_all_submissions(course_key_str, block_id, block_type):
student_id = submission['student_id']
if user_id is None:
submissions = sub_api.get_all_submissions(course_key_str, block_id, block_type)
else:
student_dict = {
'student_id': user_id,
'item_id': block_id,
'course_id': course_key_str,
'item_type': block_type,
}
submissions = sub_api.get_submissions(student_dict, limit=1)
for submission in submissions:
# If the student ID key doesn't exist, we're dealing with a single student and know the ID already.
student_id = submission.get('student_id', user_id)
if student_id not in student_submissions:
student_submissions[student_id] = [student_id] + [""] * len(blocks_to_include)
student_submissions[student_id][idx] = submission['answer']
......
<h3>Data Export</h3>
{% load i18n %}
<h3>{% trans "Data Export" %}</h3>
<p>You can export all student answers to multiple-choice questions to a CSV file here.</p>
<p>{% trans "You can export all student answers to multiple-choice questions and long-form answers to a CSV file here." %}</p>
<div class="data-export-status"></div>
<div class="data-export-actions">
<button class="data-export-download">Download result</button>
<button class="data-export-start">Start a new export</button>
<button class="data-export-cancel">Cancel current export</button>
<button class="data-export-delete">Delete 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-delete">{% trans "Delete result" %}</button>
</div>
# -*- coding: utf-8 -*-
import time
import pdb
import sys
from mock import patch, Mock
from selenium.common.exceptions import NoSuchElementException
from xblockutils.base_test import SeleniumXBlockTest
from problem_builder.data_export import DataExportBlock
......@@ -82,3 +81,11 @@ class DataExportTest(SeleniumXBlockTest):
self.wait_until_visible(download_button)
self.wait_until_visible(delete_button)
self.assertIn('A report is available for download.', status_area.text)
def test_non_staff_disabled(self):
data_export = self.go_to_view()
self.assertRaises(NoSuchElementException, data_export.find_element_by_class_name, 'data-export-start')
self.assertRaises(NoSuchElementException, data_export.find_element_by_class_name, 'data-export-cancel')
self.assertRaises(NoSuchElementException, data_export.find_element_by_class_name, 'data-export-download')
self.assertRaises(NoSuchElementException, data_export.find_element_by_class_name, 'data-export-delete')
self.assertRaises(NoSuchElementException, data_export.find_element_by_class_name, 'data-export-status')
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