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
f1d6fab2
Commit
f1d6fab2
authored
Jun 01, 2015
by
Sven Marnach
Committed by
Jonathan Piacenti
Jun 19, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Adapt code to API changes in edx-platform and edx-submissions.
parent
06b7460b
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
137 additions
and
70 deletions
+137
-70
problem_builder/data_export.py
+50
-29
problem_builder/public/css/data_export.css
+6
-0
problem_builder/public/js/data_export.js
+68
-19
problem_builder/tasks.py
+5
-4
problem_builder/templates/html/data_export.html
+8
-18
No files found.
problem_builder/data_export.py
View file @
f1d6fab2
...
@@ -60,11 +60,11 @@ class DataExportBlock(XBlock):
...
@@ -60,11 +60,11 @@ class DataExportBlock(XBlock):
""" Studio View """
""" Studio View """
# Warn the user that this block will only work from the LMS. (Since the CMS uses
# 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)
# 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>"
)
return
Fragment
(
u
'<p>Data Export Block</p><p>This block only works from the LMS.</p>'
)
def
studio_view
(
self
,
context
=
None
):
def
studio_view
(
self
,
context
=
None
):
""" 'Edit' form view in Studio """
""" 'Edit' form view in Studio """
return
Fragment
(
u
"<p>This block has no configuration options.</p>"
)
return
Fragment
(
u
'<p>This block has no configuration options.</p>'
)
def
check_pending_export
(
self
):
def
check_pending_export
(
self
):
"""
"""
...
@@ -78,27 +78,22 @@ class DataExportBlock(XBlock):
...
@@ -78,27 +78,22 @@ class DataExportBlock(XBlock):
def
_save_result
(
self
,
task_result
):
def
_save_result
(
self
,
task_result
):
""" Given an AsyncResult or EagerResult, save it. """
""" Given an AsyncResult or EagerResult, save it. """
self
.
active_export_task_id
=
""
self
.
active_export_task_id
=
''
if
task_result
.
successful
():
if
task_result
.
successful
():
if
isinstance
(
task_result
.
result
,
dict
)
and
not
task_result
.
result
.
get
(
'error'
):
if
isinstance
(
task_result
.
result
,
dict
)
and
not
task_result
.
result
.
get
(
'error'
):
self
.
last_export_result
=
task_result
.
result
self
.
last_export_result
=
task_result
.
result
else
:
else
:
self
.
last_export_result
=
{
"error"
:
u"Unexpected result: {}"
.
format
(
repr
(
task_result
.
result
))}
self
.
last_export_result
=
{
'error'
:
u'Unexpected result: {}'
.
format
(
repr
(
task_result
.
result
))}
else
:
else
:
self
.
last_export_result
=
{
"error"
:
unicode
(
task_result
.
result
)}
self
.
last_export_result
=
{
'error'
:
unicode
(
task_result
.
result
)}
def
student_view
(
self
,
context
=
None
):
def
student_view
(
self
,
context
=
None
):
""" Normal View """
""" Normal View """
# TODO: Verify instructor permissions
if
not
self
.
user_is_staff
():
# Check if any pending export has finished:
return
Fragment
(
u'<p>This interface can only be used by course staff.</p>'
)
self
.
check_pending_export
()
html
=
loader
.
render_template
(
'templates/html/data_export.html'
)
# 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
,
'download_url'
:
self
.
download_url_for_last_report
,
})
fragment
=
Fragment
(
html
)
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/data_export.js'
))
fragment
.
initialize_js
(
'DataExportBlock'
)
fragment
.
initialize_js
(
'DataExportBlock'
)
return
fragment
return
fragment
...
@@ -107,27 +102,40 @@ class DataExportBlock(XBlock):
...
@@ -107,27 +102,40 @@ class DataExportBlock(XBlock):
def
download_url_for_last_report
(
self
):
def
download_url_for_last_report
(
self
):
""" Get the URL for the last report, if any """
""" Get the URL for the last report, if any """
# Unfortunately this is a bit inefficient due to the ReportStore API
# 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
:
if
not
self
.
last_export_result
or
self
.
last_export_result
[
'error'
]
is
not
None
:
return
None
return
None
from
instructor_task.models
import
ReportStore
from
instructor_task.models
import
ReportStore
report_store
=
ReportStore
.
from_config
()
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
course_key
=
self
.
scope_ids
.
usage_id
.
course_key
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
)
return
dict
(
report_store
.
links_for
(
course_key
))
.
get
(
self
.
last_export_result
[
'report_filename'
])
def
_get_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
,
}
@XBlock.json_handler
def
get_status
(
self
,
request
,
suffix
=
''
):
return
self
.
_get_status
()
@XBlock.json_handler
@XBlock.json_handler
def
delete_export
(
self
,
request
,
suffix
=
''
):
def
delete_export
(
self
,
request
,
suffix
=
''
):
self
.
_delete_export
()
self
.
_delete_export
()
return
{
"result"
:
"ok"
}
return
self
.
_get_status
()
def
_delete_export
(
self
):
def
_delete_export
(
self
):
self
.
last_export_result
=
None
self
.
last_export_result
=
None
self
.
active_export_task_id
=
""
self
.
active_export_task_id
=
''
@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 """
if
not
self
.
user_is_staff
():
return
{
'error'
:
'permission denied'
}
from
.tasks
import
export_data
as
export_data_task
# Import here since this is edX LMS specific
from
.tasks
import
export_data
as
export_data_task
# Import here since this is edX LMS specific
# 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
())
if
async_result
.
ready
():
if
async_result
.
ready
():
...
@@ -141,16 +149,29 @@ class DataExportBlock(XBlock):
...
@@ -141,16 +149,29 @@ class DataExportBlock(XBlock):
else
:
else
:
# The task is running asynchronously. Store the result ID so we can query its progress:
# The task is running asynchronously. Store the result ID so we can query its progress:
self
.
active_export_task_id
=
async_result
.
id
self
.
active_export_task_id
=
async_result
.
id
return
{
"result"
:
"started"
}
return
self
.
_get_status
()
return
{
'result'
:
'started'
}
def
get_user_id
(
self
):
@XBlock.json_handler
"""
def
cancel_export
(
self
,
request
,
suffix
=
''
):
Get the ID of the current user.
from
.tasks
import
export_data
as
export_data_task
# Import here since this is edX LMS specific
"""
if
self
.
active_export_task_id
:
async_result
=
export_data_task
.
AsyncResult
(
self
.
active_export_task_id
)
async_result
.
revoke
()
self
.
_delete_export
()
def
_get_user_attr
(
self
,
attr
):
"""Get an attribute of the current user."""
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
if
user_service
:
if
user_service
:
# May be None when creating bok choy test fixtures
# May be None when creating bok choy test fixtures
user_id
=
user_service
.
get_current_user
()
.
opt_attrs
.
get
(
'edx-platform.user_id'
,
None
)
return
user_service
.
get_current_user
()
.
opt_attrs
.
get
(
attr
)
else
:
return
None
user_id
=
None
return
user_id
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/css/data_export.css
0 → 100644
View file @
f1d6fab2
.data-export-actions
button
{
display
:
none
;
}
.data-export-status
{
height
:
8em
;
}
problem_builder/public/js/data_export.js
View file @
f1d6fab2
function
DataExportBlock
(
runtime
,
element
)
{
function
DataExportBlock
(
runtime
,
element
)
{
"use strict"
;
"use strict"
;
var
$startExportBtn
=
$
(
".new-data-export"
,
element
);
var
$startButton
=
$
(
".data-export-start"
,
element
);
var
$deleteExportBtn
=
$
(
".delete-data-export"
,
element
);
var
$cancelButton
=
$
(
".data-export-cancel"
,
element
);
$startExportBtn
.
on
(
"click"
,
function
()
{
var
$downloadButton
=
$
(
".data-export-download"
,
element
);
var
$deleteButton
=
$
(
".data-export-delete"
,
element
);
var
status
;
function
getStatus
()
{
$
.
ajax
({
$
.
ajax
({
type
:
"POST"
,
type
:
"POST"
,
url
:
runtime
.
handlerUrl
(
element
,
'start_export'
),
url
:
runtime
.
handlerUrl
(
element
,
'get_status'
),
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
:
"{}"
,
data
:
"{}"
,
success
:
function
(
data
)
{
success
:
updateStatus
,
$deleteExportBtn
.
prop
(
'disabled'
,
true
);
},
dataType
:
"json"
,
dataType
:
"json"
,
});
});
}
function
updateStatus
(
newStatus
)
{
var
statusChanged
=
newStatus
!==
status
;
status
=
newStatus
;
if
(
status
.
export_pending
)
{
// Keep polling for status updates when an export is running.
setTimeout
(
getStatus
,
1000
);
}
if
(
statusChanged
)
updateView
();
}
function
updateView
()
{
var
$statusArea
=
$
(
".data-export-status"
,
element
);
$statusArea
.
empty
();
$startButton
.
toggle
(
!
status
.
export_pending
);
$cancelButton
.
toggle
(
status
.
export_pending
);
$downloadButton
.
toggle
(
Boolean
(
status
.
download_url
));
$deleteButton
.
toggle
(
Boolean
(
status
.
last_export_result
));
if
(
status
.
last_export_result
)
{
if
(
status
.
last_export_result
.
error
)
{
$statusArea
.
append
(
$
(
'<p>'
).
text
(
'Data export failed. Reason: '
+
status
.
last_export_result
.
error
));
}
else
{
$statusArea
.
append
(
$
(
'<p>'
).
text
(
'Date completed: '
+
status
.
last_export_result
.
report_date
));
$statusArea
.
append
(
$
(
'<p>'
).
text
(
'The report took '
+
status
.
last_export_result
.
generation_time_s
+
' seconds to generate.'
));
}
}
else
{
if
(
status
.
export_pending
)
{
$statusArea
.
append
(
$
(
'<p>'
).
text
(
'The report is currently being generated…'
));
}
else
{
$statusArea
.
append
(
$
(
'<p>'
).
text
(
'No report data available.'
));
}
}
}
function
addHandler
(
$button
,
handlerName
)
{
$button
.
on
(
"click"
,
function
()
{
$
.
ajax
({
type
:
"POST"
,
url
:
runtime
.
handlerUrl
(
element
,
handlerName
),
data
:
"{}"
,
success
:
updateStatus
,
dataType
:
"json"
,
});
});
}
addHandler
(
$startButton
,
'start_export'
);
addHandler
(
$cancelButton
,
'cancel_export'
);
addHandler
(
$deleteButton
,
'delete_export'
);
$downloadButton
.
on
(
"click"
,
function
()
{
window
.
location
.
href
=
status
.
download_url
;
});
});
getStatus
();
}
}
problem_builder/tasks.py
View file @
f1d6fab2
...
@@ -60,9 +60,10 @@ def export_data(source_block_id_str, user_id):
...
@@ -60,9 +60,10 @@ def export_data(source_block_id_str, user_id):
block_id
=
unicode
(
block
.
scope_ids
.
usage_id
.
replace
(
branch
=
None
,
version_guid
=
None
))
block_id
=
unicode
(
block
.
scope_ids
.
usage_id
.
replace
(
branch
=
None
,
version_guid
=
None
))
block_type
=
block
.
scope_ids
.
block_type
block_type
=
block
.
scope_ids
.
block_type
for
submission
in
sub_api
.
get_all_submissions
(
course_key_str
,
block_id
,
block_type
):
for
submission
in
sub_api
.
get_all_submissions
(
course_key_str
,
block_id
,
block_type
):
if
submission
.
student_id
not
in
student_submissions
:
student_id
=
submission
[
'student_id'
]
student_submissions
[
submission
.
student_id
]
=
[
submission
.
student_id
]
+
[
""
]
*
len
(
blocks_to_include
)
if
student_id
not
in
student_submissions
:
student_submissions
[
submission
.
student_id
][
idx
]
=
submission
.
answer
student_submissions
[
student_id
]
=
[
student_id
]
+
[
""
]
*
len
(
blocks_to_include
)
student_submissions
[
student_id
][
idx
]
=
submission
[
'answer'
]
# Now change from a dict to an array ordered by student ID as we generate the remaining rows:
# Now change from a dict to an array ordered by student ID as we generate the remaining rows:
for
student_id
in
sorted
(
student_submissions
.
iterkeys
()):
for
student_id
in
sorted
(
student_submissions
.
iterkeys
()):
...
@@ -71,7 +72,7 @@ def export_data(source_block_id_str, user_id):
...
@@ -71,7 +72,7 @@ def export_data(source_block_id_str, user_id):
# Generate the CSV:
# Generate the CSV:
filename
=
u"pb-data-export-{}.csv"
.
format
(
report_date
.
strftime
(
"
%
Y-
%
m-
%
d-
%
H
%
M
%
S"
))
filename
=
u"pb-data-export-{}.csv"
.
format
(
report_date
.
strftime
(
"
%
Y-
%
m-
%
d-
%
H
%
M
%
S"
))
report_store
=
ReportStore
.
from_config
()
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
report_store
.
store_rows
(
course_key
,
filename
,
rows
)
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
()
...
...
problem_builder/templates/html/data_export.html
View file @
f1d6fab2
<h3>
Data Export
</h3>
<h3>
Data Export
</h3>
<
script
>
<
p>
You can export all student answers to multiple-choice questions to a CSV file here.
</p
>
<
/script
>
<
div
class=
"data-export-status"
></div
>
<p><button
class=
"new-data-export"
>
Start a new export
</button></p>
<div
class=
"data-export-actions"
>
<button
class=
"data-export-download"
>
Download result
</button>
{% if last_export_result %}
<button
class=
"data-export-start"
>
Start a new export
</button>
<h3>
Export
</h3>
<button
class=
"data-export-cancel"
>
Cancel current export
</button>
{% if last_export_result.error %}
<button
class=
"data-export-delete"
>
Delete result
</button>
<p>
Export failed! Reason: {{ last_export_result.error }}.
</p>
</div>
{% else %}
<p>
Date: {{ last_export_result.report_date }}
</p>
<p>
Took {{ last_export_result.generation_time_s }} seconds to generate.
</p>
<p>
Download:
<a
href=
"{{download_url}}"
>
{{ last_export_result.report_filename }}
</a></p>
{% endif %}
<p><button
class=
"delete-data-export"
>
Delete export
</button></p>
{% elif export_pending %}
<p>
Report is currently being generated...
</p>
{% endif %}
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