Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xblock-poll
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
edx
xblock-poll
Commits
c7f34db7
Commit
c7f34db7
authored
Oct 13, 2017
by
Braden MacDonald
Browse files
Options
Browse Files
Download
Plain Diff
Merge v1.4: Add CSV Export
parents
b08c156e
db10cd81
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
287 additions
and
9 deletions
+287
-9
poll/poll.py
+173
-3
poll/public/css/poll.css
+2
-1
poll/public/html/poll.html
+14
-3
poll/public/html/survey.html
+14
-1
poll/public/js/poll.js
+49
-0
poll/tasks.py
+33
-0
setup.py
+1
-1
tests/unit/test_xblock_poll.py
+1
-0
No files found.
poll/poll.py
View file @
c7f34db7
...
...
@@ -24,6 +24,7 @@
from
collections
import
OrderedDict
import
functools
import
json
import
time
from
markdown
import
markdown
import
pkg_resources
...
...
@@ -37,6 +38,7 @@ from xblockutils.resources import ResourceLoader
from
xblockutils.settings
import
XBlockWithSettingsMixin
,
ThemableXBlockMixin
from
.utils
import
_
try
:
# pylint: disable=import-error
from
django.conf
import
settings
...
...
@@ -83,12 +85,122 @@ class ResourceMixin(XBlockWithSettingsMixin, ThemableXBlockMixin):
return
frag
class
CSVExportMixin
(
object
):
"""
Allows Poll or Surveys XBlocks to support CSV downloads of all users'
details per block.
"""
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_summary
,
)
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_summary
,
)
@XBlock.json_handler
def
csv_export
(
self
,
data
,
suffix
=
''
):
"""
Asynchronously export given data as a CSV file.
"""
# Launch task
from
.tasks
import
export_csv_data
# Import here since this is edX LMS specific
# Make sure we nail down our state before sending off an asynchronous task.
async_result
=
export_csv_data
.
delay
(
unicode
(
getattr
(
self
.
scope_ids
,
'usage_id'
,
None
)),
unicode
(
getattr
(
self
.
runtime
,
'course_id'
,
'course_id'
)),
)
if
not
async_result
.
ready
():
self
.
active_export_task_id
=
async_result
.
id
else
:
self
.
_store_export_result
(
async_result
)
return
self
.
_get_export_status
()
@XBlock.json_handler
def
get_export_status
(
self
,
data
,
suffix
=
''
):
"""
Return current export's pending status, previous result,
and the download URL.
"""
return
self
.
_get_export_status
()
def
_get_export_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
,
}
def
check_pending_export
(
self
):
"""
If we're waiting for an export, see if it has finished, and if so, get the result.
"""
from
.tasks
import
export_csv_data
# Import here since this is edX LMS specific
if
self
.
active_export_task_id
:
async_result
=
export_csv_data
.
AsyncResult
(
self
.
active_export_task_id
)
if
async_result
.
ready
():
self
.
_store_export_result
(
async_result
)
@property
def
download_url_for_last_report
(
self
):
""" Get the URL for the last report, if any """
from
lms.djangoapps.instructor_task.models
import
ReportStore
# pylint: disable=import-error
# 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
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
course_key
=
getattr
(
self
.
scope_ids
.
usage_id
,
'course_key'
,
None
)
return
dict
(
report_store
.
links_for
(
course_key
))
.
get
(
self
.
last_export_result
[
'report_filename'
])
def
student_module_queryset
(
self
):
from
courseware.models
import
StudentModule
# pylint: disable=import-error
return
StudentModule
.
objects
.
filter
(
course_id
=
self
.
runtime
.
course_id
,
module_state_key
=
self
.
scope_ids
.
usage_id
,
)
.
order_by
(
'-modified'
)
def
_store_export_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
prepare_data
(
self
):
"""
Return a two-dimensional list containing cells of data ready for CSV export.
"""
raise
NotImplementedError
def
get_filename
(
self
):
"""
Return a string to be used as the filename for the CSV export.
"""
raise
NotImplementedError
@XBlock.wants
(
'settings'
)
@XBlock.needs
(
'i18n'
)
class
PollBase
(
XBlock
,
ResourceMixin
,
PublishEventMixin
):
"""
Base class for Poll-like XBlocks.
"""
has_author_view
=
True
event_namespace
=
'xblock.pollbase'
private_results
=
Boolean
(
default
=
False
,
help
=
_
(
"Whether or not to display results to the user."
))
max_submissions
=
Integer
(
default
=
1
,
help
=
_
(
"The maximum number of times a user may send a submission."
))
...
...
@@ -301,7 +413,7 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return
cls
.
json_handler
(
func
)
class
PollBlock
(
PollBase
):
class
PollBlock
(
PollBase
,
CSVExportMixin
):
"""
Poll XBlock. Allows a teacher to poll users, and presents the results so
far of the poll to the user when finished.
...
...
@@ -396,6 +508,13 @@ class PollBlock(PollBase):
return
None
def
author_view
(
self
,
context
=
None
):
"""
Used to hide CSV export in Studio view
"""
context
[
'studio_edit'
]
=
True
return
self
.
student_view
(
context
)
@XBlock.supports
(
"multi_device"
)
# Mark as mobile-friendly
def
student_view
(
self
,
context
=
None
):
"""
...
...
@@ -620,8 +739,27 @@ class PollBlock(PollBase):
"""
),
]
class
SurveyBlock
(
PollBase
):
def
get_filename
(
self
):
return
u"poll-data-export-{}.csv"
.
format
(
time
.
strftime
(
"
%
Y-
%
m-
%
d-
%
H
%
M
%
S"
,
time
.
gmtime
(
time
.
time
())))
def
prepare_data
(
self
):
header_row
=
[
'user_id'
,
'username'
,
'user_email'
,
'question'
,
'answer'
]
data
=
{}
answers_dict
=
dict
(
self
.
answers
)
for
sm
in
self
.
student_module_queryset
():
choice
=
json
.
loads
(
sm
.
state
)[
'choice'
]
if
sm
.
student
.
id
not
in
data
:
data
[
sm
.
student
.
id
]
=
[
sm
.
student
.
id
,
sm
.
student
.
username
,
sm
.
student
.
email
,
self
.
question
,
answers_dict
[
choice
][
'label'
],
]
return
[
header_row
]
+
data
.
values
()
class
SurveyBlock
(
PollBase
,
CSVExportMixin
):
# pylint: disable=too-many-instance-attributes
display_name
=
String
(
default
=
_
(
'Survey'
))
...
...
@@ -656,6 +794,13 @@ class SurveyBlock(PollBase):
choices
=
Dict
(
help
=
_
(
"The user's answers"
),
scope
=
Scope
.
user_state
)
event_namespace
=
'xblock.survey'
def
author_view
(
self
,
context
=
None
):
"""
Used to hide CSV export in Studio view
"""
context
[
'studio_edit'
]
=
True
return
self
.
student_view
(
context
)
@XBlock.supports
(
"multi_device"
)
# Mark as mobile-friendly
def
student_view
(
self
,
context
=
None
):
"""
...
...
@@ -1032,3 +1177,28 @@ class SurveyBlock(PollBase):
feedback="### Thank you for running the tests."/>
"""
)
]
def
get_filename
(
self
):
return
u"survey-data-export-{}.csv"
.
format
(
time
.
strftime
(
"
%
Y-
%
m-
%
d-
%
H
%
M
%
S"
,
time
.
gmtime
(
time
.
time
())))
def
prepare_data
(
self
):
header_row
=
[
'user_id'
,
'username'
,
'user_email'
]
sorted_questions
=
sorted
(
self
.
questions
,
key
=
lambda
x
:
x
[
0
])
questions
=
[
q
[
1
][
'label'
]
for
q
in
sorted_questions
]
data
=
{}
answers_dict
=
dict
(
self
.
answers
)
for
sm
in
self
.
student_module_queryset
():
state
=
json
.
loads
(
sm
.
state
)
if
sm
.
student
.
id
not
in
data
and
state
.
get
(
'choices'
):
row
=
[
sm
.
student
.
id
,
sm
.
student
.
username
,
sm
.
student
.
email
,
]
for
q
in
sorted_questions
:
choices
=
state
.
get
(
'choices'
)
if
choices
:
choice
=
choices
[
q
[
0
]]
row
.
append
(
answers_dict
[
choice
])
data
[
sm
.
student
.
id
]
=
row
return
[
header_row
+
questions
]
+
data
.
values
()
poll/public/css/poll.css
View file @
c7f34db7
...
...
@@ -274,7 +274,8 @@ th.survey-answer {
font-weight
:
bold
;
}
.view-results-button-wrapper
{
.view-results-button-wrapper
,
.export-results-button-wrapper
{
margin-bottom
:
5px
;
text-align
:
right
;
cursor
:
pointer
;
}
...
...
poll/public/html/poll.html
View file @
c7f34db7
...
...
@@ -57,11 +57,22 @@
</div>
{% endif %}
{% if can_view_private_results %}
</div>
{% if can_view_private_results %}
<div
class=
"view-results-button-wrapper"
>
<button
class=
"view-results-button"
>
{% trans 'View results' %}
</button>
</div>
{% endif %}
</div>
</div>
{% if can_view_private_results %}
{% if not studio_edit %}
<div
class=
"export-results-button-wrapper"
>
<button
class=
"export-results-button"
>
Export results to CSV
</button>
<button
disabled
class=
"download-results-button"
>
Download CSV
</button>
<p
class=
"error-message poll-hidden"
></p>
</div>
{% else %}
<p>
Student data and results CSV available for download in the LMS.
</p>
{% endif %}
{% endif %}
poll/public/html/survey.html
View file @
c7f34db7
...
...
@@ -69,7 +69,20 @@
{% endif %}
{% if can_view_private_results %}
<div
class=
"view-results-button-wrapper"
><button
class=
"view-results-button"
>
{% trans 'View results' %}
</button></div>
<div
class=
"view-results-button-wrapper"
>
<button
class=
"view-results-button"
>
{% trans 'View results' %}
</button>
</div>
{% endif %}
</div>
</div>
{% if can_view_private_results %}
{% if not studio_edit %}
<div
class=
"export-results-button-wrapper"
>
<button
class=
"export-results-button"
>
Export results to CSV
</button>
<button
disabled
class=
"download-results-button"
>
Download CSV
</button>
<p
class=
"error-message poll-hidden"
></p>
</div>
{% else %}
<p>
Student data and results CSV available for download in the LMS.
</p>
{% endif %}
{% endif %}
poll/public/js/poll.js
View file @
c7f34db7
...
...
@@ -2,14 +2,17 @@
function
PollUtil
(
runtime
,
element
,
pollType
)
{
var
self
=
this
;
var
exportStatus
=
{};
this
.
init
=
function
()
{
// Initialization function used for both Poll Types
this
.
voteUrl
=
runtime
.
handlerUrl
(
element
,
'vote'
);
this
.
tallyURL
=
runtime
.
handlerUrl
(
element
,
'get_results'
);
this
.
csv_url
=
runtime
.
handlerUrl
(
element
,
'csv_export'
);
this
.
votedUrl
=
runtime
.
handlerUrl
(
element
,
'student_voted'
);
this
.
submit
=
$
(
'input[type=button]'
,
element
);
this
.
answers
=
$
(
'input[type=radio]'
,
element
);
this
.
errorMessage
=
$
(
'.error-message'
,
element
);
// Set up gettext in case it isn't available in the client runtime:
if
(
typeof
gettext
==
"undefined"
)
{
...
...
@@ -51,6 +54,12 @@ function PollUtil (runtime, element, pollType) {
this
.
viewResultsButton
=
$
(
'.view-results-button'
,
element
);
this
.
viewResultsButton
.
click
(
this
.
getResults
);
this
.
exportResultsButton
=
$
(
'.export-results-button'
,
element
);
this
.
exportResultsButton
.
click
(
this
.
exportCsv
);
this
.
downloadResultsButton
=
$
(
'.download-results-button'
,
element
);
this
.
downloadResultsButton
.
click
(
this
.
downloadCsv
);
return
this
.
shouldDisplayResults
();
};
...
...
@@ -185,6 +194,46 @@ function PollUtil (runtime, element, pollType) {
self
.
getResults
();
};
function
getStatus
()
{
$
.
ajax
({
type
:
'POST'
,
url
:
runtime
.
handlerUrl
(
element
,
'get_export_status'
),
data
:
'{}'
,
success
:
updateStatus
,
dataType
:
'json'
});
}
function
updateStatus
(
newStatus
)
{
var
statusChanged
=
!
_
.
isEqual
(
newStatus
,
exportStatus
);
exportStatus
=
newStatus
;
if
(
exportStatus
.
export_pending
)
{
// Keep polling for status updates when an export is running.
setTimeout
(
getStatus
,
1000
);
}
if
(
statusChanged
)
{
if
(
newStatus
.
last_export_result
.
error
)
{
self
.
errorMessage
.
text
(
error
);
self
.
errorMessage
.
show
();
}
else
{
self
.
downloadResultsButton
.
attr
(
'disabled'
,
false
);
self
.
errorMessage
.
hide
()
}
}
}
this
.
exportCsv
=
function
()
{
$
.
ajax
({
type
:
"POST"
,
url
:
self
.
csv_url
,
data
:
JSON
.
stringify
({}),
success
:
updateStatus
});
};
this
.
downloadCsv
=
function
()
{
window
.
location
=
exportStatus
.
download_url
;
};
this
.
getResults
=
function
()
{
// Used if results are not private, to show the user how other students voted.
function
adjustGaugeBackground
()
{
...
...
poll/tasks.py
0 → 100644
View file @
c7f34db7
import
time
from
celery.decorators
import
task
# pylint: disable=import-error
from
lms.djangoapps.instructor_task.models
import
ReportStore
# pylint: disable=import-error
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
# pylint: disable=import-error
from
xmodule.modulestore.django
import
modulestore
# pylint: disable=import-error
@task
()
def
export_csv_data
(
block_id
,
course_id
):
"""
Exports student answers to all supported questions to a CSV file.
"""
src_block
=
modulestore
()
.
get_item
(
UsageKey
.
from_string
(
block_id
))
start_timestamp
=
time
.
time
()
course_key
=
CourseKey
.
from_string
(
course_id
)
filename
=
src_block
.
get_filename
()
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
report_store
.
store_rows
(
course_key
,
filename
,
src_block
.
prepare_data
())
generation_time_s
=
time
.
time
()
-
start_timestamp
return
{
"error"
:
None
,
"report_filename"
:
filename
,
"start_timestamp"
:
start_timestamp
,
"generation_time_s"
:
generation_time_s
,
}
setup.py
View file @
c7f34db7
...
...
@@ -44,7 +44,7 @@ def package_data(pkg, roots):
setup
(
name
=
'xblock-poll'
,
version
=
'1.
3.5
'
,
version
=
'1.
4.0
'
,
description
=
'An XBlock for polling users.'
,
packages
=
[
'poll'
,
...
...
tests/unit/test_xblock_poll.py
View file @
c7f34db7
import
unittest
import
json
from
xblock.field_data
import
DictFieldData
from
poll.poll
import
PollBlock
,
SurveyBlock
...
...
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