Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
problem-builder
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
problem-builder
Commits
ddc822aa
Commit
ddc822aa
authored
Jun 18, 2015
by
Jonathan Piacenti
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow student access, answer submissions, and internationalization to data-export.
parent
1efe0ad9
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
82 additions
and
34 deletions
+82
-34
problem_builder/answer.py
+7
-1
problem_builder/data_export.py
+17
-11
problem_builder/public/js/data_export.js
+25
-9
problem_builder/tasks.py
+17
-5
problem_builder/templates/html/data_export.html
+7
-6
problem_builder/tests/integration/test_data_export.py
+9
-2
No files found.
problem_builder/answer.py
View file @
ddc822aa
...
...
@@ -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
()
...
...
problem_builder/data_export.py
View file @
ddc822aa
...
...
@@ -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'
)
problem_builder/public/js/data_export.js
View file @
ddc822aa
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
();
});
...
...
problem_builder/tasks.py
View file @
ddc822aa
...
...
@@ -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 stu
d
ent 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'
]
...
...
problem_builder/templates/html/data_export.html
View file @
ddc822aa
<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>
problem_builder/tests/integration/test_data_export.py
View file @
ddc822aa
# -*- 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'
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment